chromium/chrome/browser/resources/chromeos/accessibility/common/node_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 node_utils.js.
 */
SelectToSpeakNodeUtilsUnitTest = class extends SelectToSpeakE2ETest {
  /** @override */
  async setUpDeferred() {
    await super.setUpDeferred();
    await Promise.all([
      importModule('NodeUtils', '/common/node_utils.js'),
      importModule('ParagraphUtils', '/common/paragraph_utils.js'),
      importModule('WordUtils', '/common/word_utils.js'),
      importModule(
          ['createMockNode', 'generateTestNodeGroup'],
          '/common/testing/test_node_generator.js'),
    ]);
  }
};

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest', 'GetNodeVisibilityState', function() {
      const nodeWithoutRoot1 = {root: null};
      const nodeWithoutRoot2 = {root: null, state: {invisible: true}};
      assertEquals(
          NodeUtils.getNodeState(nodeWithoutRoot1),
          NodeUtils.NodeState.NODE_STATE_INVALID);
      assertEquals(
          NodeUtils.getNodeState(nodeWithoutRoot2),
          NodeUtils.NodeState.NODE_STATE_INVALID);

      const invisibleNode1 = {
        root: {},
        parent: {role: ''},
        state: {invisible: true},
      };
      // Currently nodes aren't actually marked 'invisible', so we need to
      // navigate up their tree.
      const invisibleNode2 = {
        root: {},
        parent: {role: 'window', state: {invisible: true}},
        state: {},
      };
      const invisibleNode3 = {root: {}, parent: invisibleNode2, state: {}};
      const invisibleNode4 = {root: {}, parent: invisibleNode3, state: {}};
      assertEquals(
          NodeUtils.getNodeState(invisibleNode1),
          NodeUtils.NodeState.NODE_STATE_INVISIBLE);
      assertEquals(
          NodeUtils.getNodeState(invisibleNode2),
          NodeUtils.NodeState.NODE_STATE_INVISIBLE);
      assertEquals(
          NodeUtils.getNodeState(invisibleNode3),
          NodeUtils.NodeState.NODE_STATE_INVISIBLE);

      const normalNode1 = {
        root: {},
        parent: {role: 'window', state: {}},
        state: {},
      };
      const normalNode2 = {root: {}, parent: {normalNode1}, state: {}};
      assertEquals(
          NodeUtils.getNodeState(normalNode1),
          NodeUtils.NodeState.NODE_STATE_NORMAL);
      assertEquals(
          NodeUtils.getNodeState(normalNode2),
          NodeUtils.NodeState.NODE_STATE_NORMAL);
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest', 'GetNodeVisibilityStateWithRootWebArea',
    function() {
      // Currently nodes aren't actually marked 'invisible', so we need to
      // navigate up their tree.
      const window = {root: {}, role: 'window', state: {invisible: true}};
      const rootNode =
          {root: {}, parent: window, state: {}, role: 'rootWebArea'};
      const container = {root: rootNode, parent: rootNode, state: {}};
      const node = {root: rootNode, parent: container, state: {}};
      assertEquals(
          NodeUtils.getNodeState(window),
          NodeUtils.NodeState.NODE_STATE_INVISIBLE);
      assertEquals(
          NodeUtils.getNodeState(container),
          NodeUtils.NodeState.NODE_STATE_INVISIBLE);
      assertEquals(
          NodeUtils.getNodeState(node),
          NodeUtils.NodeState.NODE_STATE_INVISIBLE);

      // Make a fake iframe in this invisible window by adding another
      // RootWebArea. The iframe has no root but is parented to the container
      // above.
      const iframeRoot = {parent: container, state: {}, role: 'rootWebArea'};
      const iframeContainer = {root: iframeRoot, parent: iframeRoot, state: {}};
      const iframeNode = {root: iframeRoot, parent: iframeContainer, state: {}};
      assertEquals(
          NodeUtils.getNodeState(iframeContainer),
          NodeUtils.NodeState.NODE_STATE_INVISIBLE);
      assertEquals(
          NodeUtils.getNodeState(iframeNode),
          NodeUtils.NodeState.NODE_STATE_INVISIBLE);

      // Make the window visible and try again.
      window.state = {};
      assertEquals(
          NodeUtils.getNodeState(window),
          NodeUtils.NodeState.NODE_STATE_NORMAL);
      assertEquals(
          NodeUtils.getNodeState(container),
          NodeUtils.NodeState.NODE_STATE_NORMAL);
      assertEquals(
          NodeUtils.getNodeState(node), NodeUtils.NodeState.NODE_STATE_NORMAL);
      assertEquals(
          NodeUtils.getNodeState(iframeContainer),
          NodeUtils.NodeState.NODE_STATE_NORMAL);
      assertEquals(
          NodeUtils.getNodeState(iframeNode),
          NodeUtils.NodeState.NODE_STATE_NORMAL);
    });

AX_TEST_F('SelectToSpeakNodeUtilsUnitTest', 'findAllMatching', function() {
  const rect = {left: 0, top: 0, width: 100, height: 100};
  const rootNode = {
    root: {},
    state: {},
    role: 'rootWebArea',
    state: {},
    location: {left: 0, top: 0, width: 600, height: 600},
  };
  const container1 = {
    root: rootNode,
    parent: rootNode,
    role: 'staticText',
    name: 'one two',
    state: {},
    location: {left: 0, top: 0, width: 200, height: 200},
  };
  const container2 = {
    root: rootNode,
    parent: rootNode,
    state: {},
    role: 'genericContainer',
    location: {left: 0, top: 0, width: 200, height: 200},
  };
  const node1 = {
    root: rootNode,
    parent: container1,
    name: 'one',
    role: 'inlineTextBox',
    state: {},
    location: {left: 50, top: 0, width: 50, height: 50},
  };
  const node2 = {
    root: rootNode,
    parent: container1,
    name: 'two',
    role: 'inlineTextBox',
    state: {},
    location: {left: 0, top: 50, width: 50, height: 50},
  };
  const node3 = {
    root: rootNode,
    parent: container1,
    value: 'text',
    role: 'textField',
    state: {},
    location: {left: 0, top: 0, width: 25, height: 25},
  };

  // Set up relationships between nodes.
  rootNode.children = [container1, container2];
  rootNode.firstChild = container1;
  container1.nextSibling = container2;
  container1.children = [node1, node2, node3];
  container1.firstChild = node1;
  node1.nextSibling = node2;
  node2.nextSibling = node3;

  // Should get both children of the first container, without getting
  // the first container itself or the empty container.
  let result = [];
  assertTrue(NodeUtils.findAllMatching(rootNode, rect, result));
  assertEquals(3, result.length);
  assertEquals(node1, result[0]);
  assertEquals(node2, result[1]);
  assertEquals(node3, result[2]);

  // If a node doesn't have a name, it should not be included.
  result = [];
  node2.name = undefined;
  node3.value = undefined;
  assertTrue(NodeUtils.findAllMatching(rootNode, rect, result));
  assertEquals(1, result.length);
  assertEquals(node1, result[0]);

  // Try a rect that only overlaps one of the children.
  result = [];
  node2.name = 'two';
  rect.height = 25;
  assertTrue(NodeUtils.findAllMatching(rootNode, rect, result));
  assertEquals(1, result.length);
  assertEquals(node1, result[0]);

  // Now just overlap a different child.
  result = [];
  rect.top = 50;
  assertTrue(NodeUtils.findAllMatching(rootNode, rect, result));
  assertEquals(1, result.length);
  assertEquals(node2, result[0]);

  // Offscreen should cause a node to be skipped.
  result = [];
  node2.state = {offscreen: true};
  assertFalse(NodeUtils.findAllMatching(rootNode, rect, result));
  assertEquals(0, result.length);

  // No location should cause a node to be skipped.
  result = [];
  node2.state = {};
  node2.location = undefined;
  assertFalse(NodeUtils.findAllMatching(rootNode, rect, result));

  // A non staticText container without a name should still have
  // children found if they are valid.
  result = [];
  const node4 = {
    root: rootNode,
    parent: container2,
    name: 'four',
    state: {},
    location: {left: 0, top: 50, width: 50, height: 50},
  };
  container2.firstChild = node4;
  assertTrue(NodeUtils.findAllMatching(rootNode, rect, result));
  assertEquals(1, result.length);
  assertEquals(node4, result[0]);

  // A non staticText container with a valid name should not be
  // read if its children are read. Children take precidence.
  result = [];
  container2.name = 'container2';
  assertTrue(NodeUtils.findAllMatching(rootNode, rect, result));
  assertEquals(1, result.length);
  assertEquals(node4, result[0]);

  // A non staticText container with a valid name which has only
  // children without names should be read instead of its children.
  result = [];
  node4.name = undefined;
  assertTrue(NodeUtils.findAllMatching(rootNode, rect, result));
  assertEquals(1, result.length);
  assertEquals(container2, result[0]);
});

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest', 'findAllMatchingWithInputs', function() {
      const rect = {left: 0, top: 0, width: 100, height: 100};
      const rootNode = {
        root: {},
        state: {},
        role: 'rootWebArea',
        location: {left: 0, top: 0, width: 600, height: 600},
      };
      const checkbox = {
        root: rootNode,
        parent: rootNode,
        role: 'checkBox',
        state: {},
        location: {left: 0, top: 0, width: 200, height: 200},
        checked: 'true',
      };
      rootNode.children = [checkbox];
      rootNode.firstChild = checkbox;

      const result = [];
      assertTrue(NodeUtils.findAllMatching(rootNode, rect, result));
      assertEquals(1, result.length);
      assertEquals(checkbox, result[0]);
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest',
    'getDeepEquivalentForSelectionDeprecatedNoChildren', function() {
      const node = {name: 'Hello, world', children: []};
      let result = NodeUtils.getDeepEquivalentForSelectionDeprecated(node, 0);
      assertEquals(node, result.node);
      assertEquals(0, result.offset);

      result = NodeUtils.getDeepEquivalentForSelectionDeprecated(node, 6);
      assertEquals(node, result.node);
      assertEquals(6, result.offset);
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest',
    'getDeepEquivalentForSelectionDeprecatedSimpleChildren', function() {
      const child1 =
          {name: 'Hello,', children: [], role: 'inlineTextBox', state: {}};
      const child2 =
          {name: ' world', children: [], role: 'inlineTextBox', state: {}};
      const root = {
        name: 'Hello, world',
        children: [child1, child2],
        role: 'staticText',
        state: {},
      };
      child1.parent = root;
      child2.parent = root;
      let result =
          NodeUtils.getDeepEquivalentForSelectionDeprecated(root, 0, true);
      assertEquals(child1, result.node);
      assertEquals(0, result.offset);

      // Get the last index of the first child
      result =
          NodeUtils.getDeepEquivalentForSelectionDeprecated(root, 5, false);
      assertEquals(child1, result.node);
      assertEquals(5, result.offset);

      // Get the first index of the second child
      result = NodeUtils.getDeepEquivalentForSelectionDeprecated(root, 6, true);
      assertEquals(child2, result.node);
      assertEquals(0, result.offset);

      result = NodeUtils.getDeepEquivalentForSelectionDeprecated(root, 9, true);
      assertEquals(child2, result.node);
      assertEquals(3, result.offset);
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest',
    'getDeepEquivalentForSelectionDeprecatedComplexChildren', function() {
      const child1 =
          {name: 'Hello', children: [], role: 'inlineTextBox', state: {}};
      // Empty name
      const child2 =
          {name: undefined, children: [], role: 'inlineTextBox', state: {}};
      const child3 =
          {name: ',', children: [], role: 'inlineTextBox', state: {}};
      const child4 = {
        name: 'Hello,',
        children: [child1, child2, child3],
        role: 'staticText',
        state: {},
        firstChild: child1,
        lastChild: child3,
      };
      child1.parent = child4;
      child2.parent = child4;
      child3.parent = child4;

      const child5 =
          {name: ' ', children: [], role: 'inlineTextBox', state: {}};
      const child6 =
          {name: 'world', children: [], role: 'inlineTextBox', state: {}};
      const child7 = {
        name: ' world',
        children: [child5, child6],
        role: 'staticText',
        state: {},
        firstChild: child5,
        lastChild: child6,
      };
      child5.parent = child7;
      child6.parent = child7;

      const root = {
        name: undefined,
        children: [child4, child7],
        role: 'genericContainer',
        state: {},
        firstChild: child4,
        lastChild: child7,
      };
      child4.parent = root;
      child7.parent = root;

      let result =
          NodeUtils.getDeepEquivalentForSelectionDeprecated(root, 0, true);
      assertEquals(child1, result.node);
      assertEquals(0, result.offset);

      result = NodeUtils.getDeepEquivalentForSelectionDeprecated(root, 1, true);
      assertEquals(child5, result.node);
      assertEquals(0, result.offset);

      result =
          NodeUtils.getDeepEquivalentForSelectionDeprecated(root, 2, false);
      assertEquals(child6, result.node);
      assertEquals(5, result.offset);

      result =
          NodeUtils.getDeepEquivalentForSelectionDeprecated(child4, 2, true);
      assertEquals(child1, result.node);
      assertEquals(2, result.offset);

      result =
          NodeUtils.getDeepEquivalentForSelectionDeprecated(child4, 5, true);
      assertEquals(child3, result.node);
      assertEquals(0, result.offset);
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest', 'sortSvgNodesByReadingOrder', function() {
      const svgRootNode = {role: 'svgRoot'};
      const gNode1 = {
        role: 'genericContainer',
        parent: svgRootNode,
        unclippedLocation: {left: 300, top: 10, width: 100, height: 50},
      };
      const gNode2 = {
        role: 'genericContainer',
        parent: svgRootNode,
        unclippedLocation: {left: 20, top: 10, width: 100, height: 50},
      };
      const textNode1 = {
        role: 'staticText',
        parent: gNode2,
        unclippedLocation: {left: 50, top: 10, width: 20, height: 50},
        name: 'one',
      };
      const textNode2 = {
        role: 'staticText',
        parent: gNode1,
        unclippedLocation: {left: 300, top: 10, width: 20, height: 50},
        name: 'two',
      };
      const textNode3 = {
        role: 'staticText',
        parent: gNode1,
        unclippedLocation: {left: 350, top: 10, width: 20, height: 50},
        name: 'three',
      };

      const nodes = [textNode3, textNode2, textNode1];
      NodeUtils.sortSvgNodesByReadingOrder(nodes);
      assertEquals(nodes[0].name, 'one');
      assertEquals(nodes[1].name, 'two');
      assertEquals(nodes[2].name, 'three');
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest', 'sortNodesByReadingOrderMultipleSVGs',
    function() {
      const textNode1 = {role: 'staticText', name: 'Text Node 1'};
      const svg1RootNode = {role: 'svgRoot'};
      const svg1Node1 = {
        role: 'staticText',
        parent: svg1RootNode,
        unclippedLocation: {left: 0, top: 10, width: 20, height: 50},
        name: 'SVG 1 Node 1',
      };
      const svg1Node2 = {
        role: 'staticText',
        parent: svg1RootNode,
        unclippedLocation: {left: 50, top: 10, width: 20, height: 50},
        name: 'SVG 1 Node 2',
      };
      const textNode2 = {role: 'staticText', name: 'Text Node 2'};
      const svg2RootNode = {role: 'svgRoot'};
      const svg2Node1 = {
        role: 'staticText',
        parent: svg2RootNode,
        unclippedLocation: {left: 300, top: 10, width: 20, height: 50},
        name: 'SVG 2 Node 1',
      };
      const svg2Node2 = {
        role: 'staticText',
        parent: svg2RootNode,
        unclippedLocation: {left: 350, top: 10, width: 20, height: 50},
        name: 'SVG 2 Node 2',
      };
      const textNode3 = {role: 'staticText', name: 'Text Node 3'};

      const nodes = [
        textNode1,
        svg1Node2,
        svg1Node1,
        textNode2,
        svg2Node2,
        svg2Node1,
        textNode3,
      ];
      NodeUtils.sortSvgNodesByReadingOrder(nodes);

      assertEquals(nodes[0].name, 'Text Node 1');
      assertEquals(nodes[1].name, 'SVG 1 Node 1');
      assertEquals(nodes[2].name, 'SVG 1 Node 2');
      assertEquals(nodes[3].name, 'Text Node 2');
      assertEquals(nodes[4].name, 'SVG 2 Node 1');
      assertEquals(nodes[5].name, 'SVG 2 Node 2');
      assertEquals(nodes[6].name, 'Text Node 3');
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest', 'GetAllNodesInParagraph', 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 text4 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 4'});
      const text5 = createMockNode(
          {role: 'staticText', parent: paragraph2, root, name: 'Line 5'});

      let result = NodeUtils.getAllNodesInParagraph(text1);
      assertEquals(result.length, 2);
      assertEquals(result[0], text1);
      assertEquals(result[1], text2);

      result = NodeUtils.getAllNodesInParagraph(text2);
      assertEquals(result.length, 2);
      assertEquals(result[0], text1);
      assertEquals(result[1], text2);

      result = NodeUtils.getAllNodesInParagraph(text3);
      assertEquals(result.length, 3);
      assertEquals(result[0], text3);
      assertEquals(result[1], text4);
      assertEquals(result[2], text5);

      result = NodeUtils.getAllNodesInParagraph(text4);
      assertEquals(result.length, 3);
      assertEquals(result[0], text3);
      assertEquals(result[1], text4);
      assertEquals(result[2], text5);

      result = NodeUtils.getAllNodesInParagraph(text5);
      assertEquals(result.length, 3);
      assertEquals(result[0], text3);
      assertEquals(result[1], text4);
      assertEquals(result[2], text5);
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest', 'getPositionFromNodeGroup', function() {
      // The nodeGroup has four inline text nodes and one static text node.
      // Their starting indexes are 0, 9, 20, 30, and 51. The first and the
      // second inline text nodes belong to one parent, and the third and the
      // forth inline text nodes belong to another parent.
      const nodeGroup = generateTestNodeGroup();

      let testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 0 /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[0].node.children[0]);
      assertEquals(testPosition.offset, 0);

      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 4 /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[0].node.children[0]);
      assertEquals(testPosition.offset, 4);

      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 10 /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[0].node.children[1]);
      assertEquals(testPosition.offset, 1);

      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 20 /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[1].node.children[0]);
      assertEquals(testPosition.offset, 0);

      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 30 /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[1].node.children[1]);
      assertEquals(testPosition.offset, 0);

      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 39 /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[1].node.children[1]);
      assertEquals(testPosition.offset, 9);

      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 52 /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[2].node);
      assertEquals(testPosition.offset, 1);

      // The index is out of the text of the node group, fallback to the end.
      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 100 /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[2].node);
      assertEquals(testPosition.offset, 18);

      // The index is out of the text of the node group, fall back to the start.
      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 100 /* charIndex */, false /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[0].node.children[0]);
      assertEquals(testPosition.offset, 0);

      // The index is undefined, fallback to the end.
      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, undefined /* charIndex */, true /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[2].node);
      assertEquals(testPosition.offset, 18);

      // The index is undefined, fall back to the start.
      testPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, undefined /* charIndex */, false /* fallbackToEnd */);
      assertEquals(testPosition.node, nodeGroup.nodes[0].node.children[0]);
      assertEquals(testPosition.offset, 0);
    });

AX_TEST_F(
    'SelectToSpeakNodeUtilsUnitTest', 'getDirectionBetweenPositions',
    function() {
      // The nodeGroup has four inline text nodes and one static text node.
      // Their starting indexes are 0, 9, 20, 30, and 51. The first and the
      // second inline text nodes belong to one parent, and the third and the
      // forth inline text nodes belong to another parent.
      const nodeGroup = generateTestNodeGroup();

      let startPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 0, true /* fallbackToEnd */);
      let endPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 1, true /* fallbackToEnd */);
      assertEquals(
          NodeUtils.getDirectionBetweenPositions(startPosition, endPosition),
          constants.Dir.FORWARD);

      startPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 1, true /* fallbackToEnd */);
      endPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 10, true /* fallbackToEnd */);
      assertEquals(
          NodeUtils.getDirectionBetweenPositions(startPosition, endPosition),
          constants.Dir.FORWARD);

      startPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 20, true /* fallbackToEnd */);
      endPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 6, true /* fallbackToEnd */);
      assertEquals(
          NodeUtils.getDirectionBetweenPositions(startPosition, endPosition),
          constants.Dir.BACKWARD);

      startPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 8, true /* fallbackToEnd */);
      endPosition = NodeUtils.getPositionFromNodeGroup(
          nodeGroup, 8, true /* fallbackToEnd */);
      assertEquals(
          NodeUtils.getDirectionBetweenPositions(startPosition, endPosition),
          constants.Dir.BACKWARD);
    });