chromium/chrome/browser/resources/chromeos/accessibility/switch_access/nodes/desktop_node.ts

// 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.

import {TestImportManager} from '/common/testing/test_import_manager.js';

import {Navigator} from '../navigator.js';
import {SwitchAccess} from '../switch_access.js';
import {ErrorType} from '../switch_access_constants.js';

import {BasicNode, BasicRootNode} from './basic_node.js';
import {SAChildNode, SARootNode} from './switch_access_node.js';

type AutomationNode = chrome.automation.AutomationNode;

/**
 * This class handles interactions with the desktop automation node.
 */
export class DesktopNode extends BasicRootNode {
  // ================= General methods =================

  override equals(other: SARootNode): boolean {
    // The underlying automation tree only has one desktop node, so all
    // DesktopNode instances are equal.
    return other instanceof DesktopNode;
  }

  override isValidGroup(): boolean {
    return true;
  }

  override refresh(): void {
    // Find the currently focused child.
    let focusedChild: SAChildNode | null = null;
    for (const child of this.children) {
      if (child.isFocused()) {
        focusedChild = child;
        break;
      }
    }

    // Update this DesktopNode's children.
    const childConstructor = (node: AutomationNode): BasicNode =>
        BasicNode.create(node, this);
    DesktopNode.findAndSetChildren(this, childConstructor);

    // Set the new instance of that child to be the focused node.
    for (const child of this.children) {
      if (child.isEquivalentTo(focusedChild)) {
        Navigator.byItem.forceFocusedNode(child);
        return;
      }
    }

    // If the previously focused node no longer exists, focus the first node in
    // the group.
    Navigator.byItem.forceFocusedNode(this.children[0]);
  }

  // ================= Static methods =================

  static build(desktop: AutomationNode): DesktopNode {
    const root = new DesktopNode(desktop);
    const childConstructor = (autoNode: AutomationNode): BasicNode =>
        BasicNode.create(autoNode, root);

    DesktopNode.findAndSetChildren(root, childConstructor);
    return root;
  }

  static override findAndSetChildren(
      root: DesktopNode,
      childConstructor: (node: AutomationNode) => SAChildNode): void {
    const interestingChildren = BasicRootNode.getInterestingChildren(root);

    if (interestingChildren.length < 1) {
      // If the desktop node does not behave as expected, we have no basis for
      // recovering. Wait for the next user input.
      throw SwitchAccess.error(
          ErrorType.MALFORMED_DESKTOP,
          'Desktop node must have at least 1 interesting child.',
          false /* shouldRecover */);
    }

    // TODO(crbug.com/40706137): Add hittest intervals to new children which are
    // SwitchAccessPredicate.isWindow to check whether those children are
    // occluded or visible. Remove any intervals on the previous window
    // children before reassigning root.children.
    root.children = interestingChildren.map(childConstructor);
  }
}

TestImportManager.exportForTesting(DesktopNode);