chromium/chrome/browser/resources/chromeos/accessibility/switch_access/item_scan_manager_test.js

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

GEN_INCLUDE(['switch_access_e2e_test_base.js']);

/** Test fixture for the item scan manager. */
SwitchAccessItemScanManagerTest = class extends SwitchAccessE2ETest {
  /** @override */
  async setUpDeferred() {
    await super.setUpDeferred();
    globalThis.MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;

    BackButtonNode
        .locationForTesting = {top: 10, left: 10, width: 20, height: 20};
  }

  moveToPageContents(pageContents) {
    const cache = new SACache();
    if (!SwitchAccessPredicate.isGroup(pageContents, null, cache)) {
      pageContents =
          new AutomationTreeWalker(
              pageContents, constants.Dir.FORWARD,
              {visit: node => SwitchAccessPredicate.isGroup(node, null, cache)})
              .next()
              .node;
    }
    assertNotNullNorUndefined(
        pageContents, 'Could not find group corresponding to page contents');
    Navigator.byItem.moveTo_(pageContents);
    Navigator.byItem.enterGroup();
  }
};

function currentNode() {
  return Navigator.byItem.node_;
}

AX_TEST_F('SwitchAccessItemScanManagerTest', 'MoveTo', async function() {
  const website = `<div id="outerGroup">
                     <div id="group">
                       <input type="text">
                       <input type="range">
                     </div>
                     <button></button>
                   </div>`;
  const rootWebArea = await this.runWithLoadedTree(website);
  const desktop = rootWebArea.parent.root;
  const textFields =
      desktop.findAll({role: chrome.automation.RoleType.TEXT_FIELD});
  assertTrue(
      textFields.length === 2 || textFields.length === 3,
      'Should be exactly 2 or 3 text fields.');
  const omnibar = textFields[0];
  const textInput = textFields[textFields.length - 1];
  const sliders = desktop.findAll({role: chrome.automation.RoleType.SLIDER});
  assertEquals(1, sliders.length, 'Should be exactly 1 slider.');
  const slider = sliders[0];
  const group = this.findNodeById('group');
  const outerGroup = this.findNodeById('outerGroup');

  Navigator.byItem.moveTo_(omnibar);
  assertEquals(
      chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role,
      'Did not successfully move to the omnibar');
  assertFalse(
      Navigator.byItem.group_.isEquivalentTo(group),
      'Omnibar is in the group in page contents somehow');
  assertFalse(
      Navigator.byItem.group_.isEquivalentTo(outerGroup),
      'Omnibar is in the outer group in page contents somehow');
  const grandGroup = Navigator.byItem.history_.peek().group;
  assertFalse(
      grandGroup.isEquivalentTo(group),
      'Group stack contains the group from page contents');
  assertFalse(
      grandGroup.isEquivalentTo(outerGroup),
      'Group stack contains the outer group from page contents');

  Navigator.byItem.moveTo_(textInput);
  assertEquals(
      chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role,
      'Did not successfully move to the text input');
  assertTrue(
      Navigator.byItem.group_.isEquivalentTo(group),
      'Group node was not successfully populated');
  assertTrue(
      Navigator.byItem.history_.peek().group.isEquivalentTo(outerGroup),
      'History was not built properly');

  Navigator.byItem.moveTo_(slider);
  assertEquals(
      chrome.automation.RoleType.SLIDER, Navigator.byItem.node_.role,
      'Did not successfully move to the slider');

  Navigator.byItem.moveTo_(group);
  assertTrue(Navigator.byItem.node_.isGroup(), 'Current node is not a group');
  assertTrue(
      Navigator.byItem.node_.isEquivalentTo(group),
      'Did not find the right group');
});

AX_TEST_F('SwitchAccessItemScanManagerTest', 'JumpTo', async function() {
  const website = `<div id="group1">
                     <input id="testinput" type="text">
                     <button></button>
                   </div>
                   <div id="group2">
                     <button></button>
                     <button></button>
                   </div>`;
  const rootWebArea = await this.runWithLoadedTree(website);
  const desktop = rootWebArea.parent.root;
  const textInput = this.findNodeById('testinput');
  assertNotNullNorUndefined(textInput, 'Text field is undefined');
  const group1 = this.findNodeById('group1');
  const group2 = this.findNodeById('group2');

  Navigator.byItem.moveTo_(textInput);
  const textInputNode = Navigator.byItem.node_;
  assertEquals(
      chrome.automation.RoleType.TEXT_FIELD, textInputNode.role,
      'Did not successfully move to the text input');
  assertTrue(
      Navigator.byItem.group_.isEquivalentTo(group1),
      'Group is initialized in an unexpected manner');

  Navigator.byItem.jumpTo_(BasicRootNode.buildTree(group2));
  assertFalse(
      Navigator.byItem.group_.isEquivalentTo(group1), 'Jump did nothing');
  assertTrue(
      Navigator.byItem.group_.isEquivalentTo(group2),
      'Jumped to the wrong group');

  Navigator.byItem.exitGroup_();
  assertTrue(
      Navigator.byItem.group_.isEquivalentTo(group1),
      'Did not jump back to the right group.');
});

AX_TEST_F('SwitchAccessItemScanManagerTest', 'SelectButton', async function() {
  const website = `<button id="test" aria-pressed=false>First Button</button>
      <button>Second Button</button>
      <script>
        let state = false;
        let button = document.getElementById("test");
        button.onclick = () => {
          state = !state;
          button.setAttribute("aria-pressed", state);
        };
      </script>`;

  const pageContents = await this.runWithLoadedTree(website);
  this.moveToPageContents(pageContents);

  const node = currentNode().automationNode;
  assertNotNullNorUndefined(node, 'Node is invalid');
  assertEquals(node.name, 'First Button', 'Did not find the right node');

  node.addEventListener(
      chrome.automation.EventType.CHECKED_STATE_CHANGED,
      this.newCallback(event => {
        assertEquals(
            node.name, event.target.name,
            'Checked state changed on unexpected node');
      }));

  Navigator.byItem.node_.performAction('select');
});

AX_TEST_F('SwitchAccessItemScanManagerTest', 'EnterGroup', async function() {
  const website = `<div id="group">
                     <button></button>
                     <button></button>
                   </div>
                   <input type="range">`;
  const rootWebArea = await this.runWithLoadedTree(website);
  const targetGroup = this.findNodeById('group');
  Navigator.byItem.moveTo_(targetGroup);

  const originalGroup = Navigator.byItem.group_;
  assertEquals(
      Navigator.byItem.node_.automationNode.htmlId, 'group',
      'Did not move to group properly');

  Navigator.byItem.enterGroup();
  assertEquals(
      chrome.automation.RoleType.BUTTON, Navigator.byItem.node_.role,
      'Current node is not a button');
  assertTrue(
      Navigator.byItem.group_.isEquivalentTo(targetGroup),
      'Target group was not entered');

  Navigator.byItem.exitGroup_();
  assertTrue(
      originalGroup.equals(Navigator.byItem.group_),
      'Did not move back to the original group');
});

AX_TEST_F('SwitchAccessItemScanManagerTest', 'MoveForward', async function() {
  const website = `<div>
                     <button id="button1"></button>
                     <button id="button2"></button>
                     <button id="button3"></button>
                   </div>`;
  const rootWebArea = await this.runWithLoadedTree(website);
  Navigator.byItem.moveTo_(this.findNodeById('button1'));
  const button1 = Navigator.byItem.node_;
  assertFalse(
      button1 instanceof BackButtonNode,
      'button1 should not be a BackButtonNode');
  assertEquals(
      'button1', button1.automationNode.htmlId, 'Current node is not button1');

  Navigator.byItem.moveForward();
  assertFalse(
      button1.equals(Navigator.byItem.node_),
      'Still on button1 after moveForward()');
  const button2 = Navigator.byItem.node_;
  assertFalse(
      button2 instanceof BackButtonNode,
      'button2 should not be a BackButtonNode');
  assertEquals(
      'button2', button2.automationNode.htmlId, 'Current node is not button2');

  Navigator.byItem.moveForward();
  assertFalse(
      button1.equals(Navigator.byItem.node_),
      'Unexpected navigation to button1');
  assertFalse(
      button2.equals(Navigator.byItem.node_),
      'Still on button2 after moveForward()');
  const button3 = Navigator.byItem.node_;
  assertFalse(
      button3 instanceof BackButtonNode,
      'button3 should not be a BackButtonNode');
  assertEquals(
      'button3', button3.automationNode.htmlId, 'Current node is not button3');

  Navigator.byItem.moveForward();
  assertTrue(
      Navigator.byItem.node_ instanceof BackButtonNode,
      'BackButtonNode should come after button3');

  Navigator.byItem.moveForward();
  assertTrue(
      button1.equals(Navigator.byItem.node_),
      'button1 should come after the BackButtonNode');
});

AX_TEST_F('SwitchAccessItemScanManagerTest', 'MoveBackward', async function() {
  const website = `<div>
                     <button id="button1"></button>
                     <button id="button2"></button>
                     <button id="button3"></button>
                   </div>`;
  const rootWebArea = await this.runWithLoadedTree(website);
  Navigator.byItem.moveTo_(this.findNodeById('button1'));
  const button1 = Navigator.byItem.node_;
  assertFalse(
      button1 instanceof BackButtonNode,
      'button1 should not be a BackButtonNode');
  assertEquals(
      'button1', button1.automationNode.htmlId, 'Current node is not button1');

  Navigator.byItem.moveBackward();
  assertTrue(
      Navigator.byItem.node_ instanceof BackButtonNode,
      'BackButtonNode should come before button1');

  Navigator.byItem.moveBackward();
  assertFalse(
      button1.equals(Navigator.byItem.node_),
      'Unexpected navigation to button1');
  const button3 = Navigator.byItem.node_;
  assertFalse(
      button3 instanceof BackButtonNode,
      'button3 should not be a BackButtonNode');
  assertEquals(
      'button3', button3.automationNode.htmlId, 'Current node is not button3');

  Navigator.byItem.moveBackward();
  assertFalse(
      button3.equals(Navigator.byItem.node_),
      'Still on button3 after moveBackward()');
  assertFalse(button1.equals(Navigator.byItem.node_), 'Skipped button2');
  const button2 = Navigator.byItem.node_;
  assertFalse(
      button2 instanceof BackButtonNode,
      'button2 should not be a BackButtonNode');
  assertEquals(
      'button2', button2.automationNode.htmlId, 'Current node is not button2');

  Navigator.byItem.moveBackward();
  assertTrue(
      button1.equals(Navigator.byItem.node_),
      'button1 should come before button2');
});

AX_TEST_F(
    'SwitchAccessItemScanManagerTest', 'NodeUndefinedBeforeTreeChangeRemoved',
    async function() {
      const website = `<div>
                     <button id="button1"></button>
                   </div>`;
      const rootWebArea = await this.runWithLoadedTree(website);
      Navigator.byItem.moveTo_(this.findNodeById('button1'));
      const button1 = Navigator.byItem.node_;
      assertFalse(
          button1 instanceof BackButtonNode,
          'button1 should not be a BackButtonNode');
      assertEquals(
          'button1', button1.automationNode.htmlId,
          'Current node is not button1');

      // Simulate the underlying node's deletion. Note that this is different
      // than an orphaned node (which can have a valid AutomationNode
      // instance, but no backing C++ object, so attributes returned like role
      // are undefined).
      Navigator.byItem.node_.baseNode_ = undefined;

      // Tree change removed gets sent by C++ after the tree has already
      // applied changes (so this comes after the above clearing).
      Navigator.byItem.onTreeChange_(
          {type: chrome.automation.TreeChangeType.NODE_REMOVED});
    });

// TODO(crbug.com/336827654): Investigate failures.
AX_TEST_F(
  'SwitchAccessItemScanManagerTest', 'DISABLED_ScanAndTypeVirtualKeyboard',
    async function() {
      const website = `<input type="text" id="testinput"></input>`;
      const rootWebArea = await this.runWithLoadedTree(website);

      // Move to the text field.
      Navigator.byItem.moveTo_(this.findNodeById('testinput'));
      const input = Navigator.byItem.node_;
      assertEquals(
          'testinput', input.automationNode.htmlId,
          'Current node is not input');
      input.performAction(MenuAction.KEYBOARD);

      const keyboard =
          await this.untilFocusIs({role: chrome.automation.RoleType.KEYBOARD});
      keyboard.performAction('select');

      const key = await this.untilFocusIs({instance: KeyboardNode});

      key.performAction('select');

      if (input.automationNode.value !== 'q') {
        // Wait for the potential value change.
        await new Promise(resolve => {
          input.automationNode.addEventListener(
              chrome.automation.EventType.VALUE_CHANGED, event => {
                if (event.target.value === 'q') {
                  resolve();
                }
              });
        });
      }
    });

// TODO(crbug.com/40946640): Test is flaky.
AX_TEST_F(
    'SwitchAccessItemScanManagerTest', 'DISABLED_DismissVirtualKeyboard',
    async function() {
      const website =
          `<input type="text" id="testinput"></input><button>ok</button>`;
      const rootWebArea = await this.runWithLoadedTree(website);

      // Move to the text field.
      Navigator.byItem.moveTo_(this.findNodeById('testinput'));
      const input = Navigator.byItem.node_;
      assertEquals(
          'testinput', input.automationNode.htmlId,
          'Current node is not input');
      input.performAction(MenuAction.KEYBOARD);

      const keyboard =
          await this.untilFocusIs({role: chrome.automation.RoleType.KEYBOARD});
      keyboard.performAction('select');

      // Grab the key.
      const key = await this.untilFocusIs({instance: KeyboardNode});

      // Simulate a page focusing the ok button.
      const okButton = rootWebArea.find({attributes: {name: 'ok'}});
      okButton.focus();

      // Wait for the keyboard to become invisible and the ok button to be
      // focused by automation.
      await new Promise(
          resolve => okButton.addEventListener(
              chrome.automation.EventType.FOCUS, resolve));
      await new Promise(resolve => {
        keyboard.automationNode.addEventListener(
            chrome.automation.EventType.STATE_CHANGED, event => {
              if (event.target.role === chrome.automation.RoleType.KEYBOARD &&
                  event.target.state.invisible) {
                resolve();
              }
            });
      });

      // We should end up back on the focused button in SA.
      const button =
          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
      assertEquals('ok', button.automationNode.name);
    });

// TODO(crbug.com/1260231): Test is flaky.
AX_TEST_F(
    'SwitchAccessItemScanManagerTest', 'DISABLED_ChildrenChangedDoesNotRefresh',
    async function() {
      const website = `
    <div id="slider" role="slider">
      <div role="group"><div></div></div>
    </div>
    <button>done</button>
  `;
      const rootWebArea = await this.runWithLoadedTree(website);
      // SA initially focuses this node in Ash Chrome; wait for it first.
      await new Promise(resolve => {
        chrome.commandLinePrivate.hasSwitch(
            'lacros-chrome-path', async hasLacrosChromePath => {
              if (!hasLacrosChromePath) {
                await this.untilFocusIs(
                    {className: 'BrowserNonClientFrameViewChromeOS'});
              }
              resolve();
            });
      });

      // Move to the slider.
      Navigator.byItem.moveTo_(this.findNodeById('slider'));
      const slider = Navigator.byItem.node_;
      assertEquals(
          'slider', slider.automationNode.htmlId, 'Current node is not slider');

      // Trigger a children changed on the group.
      const automationGroup =
          rootWebArea.find({role: chrome.automation.RoleType.GROUP});
      assertTrue(Boolean(automationGroup));
      const group = Navigator.byItem.group_;
      assertTrue(Boolean(group));
      const handler = group.childrenChangedHandler_;
      assertTrue(Boolean(handler));

      // Fake a children changed event.
      handler.eventStack_ = [{
        type: chrome.automation.EventType.CHILDREN_CHANGED,
        target: automationGroup,
      }];
      handler.handleEvent_();

      // This subtree is not interesting, so it should not have triggered a
      // complete refresh of the SA tree.
      assertEquals(slider, Navigator.byItem.node_);
    });

AX_TEST_F('SwitchAccessItemScanManagerTest', 'InitialFocus', async function() {
  const website = `<input></input><button autofocus></button>`;
  const rootWebArea = await this.runWithLoadedTree(website);
  // The button should have initial focus. This ensures we move past the
  // focus event below.
  const button =
      await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});

  // Build a new ItemScanManager to see what it sets as the initial node.
  const desktop = rootWebArea.parent.root;
  assertEquals(
      chrome.automation.RoleType.DESKTOP, desktop.role,
      `Unexpected desktop ${desktop.toString()}`);
  const manager = new ItemScanManager(desktop);
  manager.start();
  assertEquals(
      button.automationNode, manager.node_.automationNode,
      `Unexpected focus ${manager.node_.debugString()}`);
});


AX_TEST_F(
    'SwitchAccessItemScanManagerTest', 'SyncFocusToNewWindow',
    async function() {
      const website1 = `<button autofocus>one</button>`;
      const website2 = `<button autofocus>two</button>`;
      await this.runWithLoadedTree(website1);
      // Wait for the first button to get SA focused.
      const button1 = await this.untilFocusIs(
          {role: chrome.automation.RoleType.BUTTON, name: 'one'});

      // Launch a new browser window and load up the second site.
      EventGenerator.sendKeyPress(KeyCode.N, {ctrl: true});
      await this.runWithLoadedTree(website2);
      // Wait for the second button to get SA focused.
      const button2 = await this.untilFocusIs(
          {role: chrome.automation.RoleType.BUTTON, name: 'two'});

      // Do a search for the title bar nodes for each of the browser windows. We
      // do this by walking up to the widget for each window from the buttons.
      let widget1 = button1.automationNode;
      while (widget1.role !== chrome.automation.RoleType.WINDOW ||
             widget1.className !== 'Widget') {
        widget1 = widget1.parent;
      }
      assertTrue(Boolean(widget1));

      let widget2 = button2.automationNode;
      while (widget2.role !== chrome.automation.RoleType.WINDOW ||
             widget2.className !== 'Widget') {
        widget2 = widget2.parent;
      }
      assertTrue(Boolean(widget2));

      const titleBar1 =
          widget1.find({role: chrome.automation.RoleType.TITLE_BAR});
      assertTrue(Boolean(titleBar1));
      const titleBar2 =
          widget2.find({role: chrome.automation.RoleType.TITLE_BAR});
      assertTrue(Boolean(titleBar2));

      // The focus is currently on widget2 (since button2 has focus). Start with
      // focusing widget1 which should occur as a result of moving SA to title
      // bar 1.
      Navigator.byItem.moveTo_(titleBar1);

      // Simulate entering this group to trigger the focus.
      Navigator.byItem.enterGroup();

      // Note that this and the second instance below is system OS focus, but
      // doesn't impact SA focus to preserve current behavior and prevent
      // flickering.
      let currentFocus = await new Promise(r => {
        widget1.addEventListener(
            chrome.automation.EventType.FOCUS, e => r(e.target));
      });
      assertEquals(currentFocus, button1.automationNode);

      // Now, switch to widget2 by moving to title bar 2.
      Navigator.byItem.moveTo_(titleBar2);

      // Simulate entering this group to trigger the focus.
      Navigator.byItem.enterGroup();

      currentFocus = await new Promise(r => {
        widget2.addEventListener(
            chrome.automation.EventType.FOCUS, e => r(e.target));
      });
      assertEquals(currentFocus, button2.automationNode);
    });

// TODO(crbug.com/1219067): Unflake.
AX_TEST_F(
    'SwitchAccessItemScanManagerTest', 'DISABLED_LockScreenBlocksUserSession',
    async function() {
      const website = `<button autofocus>kitties!</button>`;
      await this.runWithLoadedTree(website);
      let button =
          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
      assertEquals('kitties!', button.automationNode.name);

      // Lock the screen.
      EventGenerator.sendKeyPress(KeyCode.L, {search: true});

      // Wait for focus to move to the password field.
      await this.untilFocusIs({
        role: chrome.automation.RoleType.TEXT_FIELD,
        name: 'Password for [email protected]',
      });

      // The button is no longer in the tree because the screen is locked.
      const predicate = node => node.name === 'kitties!' &&
          node.role === chrome.automation.RoleType.BUTTON;
      assertNotNullNorUndefined(
          this.desktop_, 'this.desktop_ is null or undefined.');
      const treeWalker = new AutomationTreeWalker(
          this.desktop_, constants.Dir.FORWARD, {visit: predicate});
      const node = treeWalker.next().node;
      assertEquals(null, node);

      // Log in again and confirm that the button is back and gets focus
      // again.
      EventGenerator.sendKeyPress(KeyCode.T);
      EventGenerator.sendKeyPress(KeyCode.E);
      EventGenerator.sendKeyPress(KeyCode.S);
      EventGenerator.sendKeyPress(KeyCode.T);
      EventGenerator.sendKeyPress(KeyCode.ZERO);
      EventGenerator.sendKeyPress(KeyCode.ZERO);
      EventGenerator.sendKeyPress(KeyCode.ZERO);
      EventGenerator.sendKeyPress(KeyCode.ZERO);
      EventGenerator.sendKeyPress(KeyCode.RETURN);

      button =
          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
      assertEquals('kitties!', button.automationNode.name);
    });