910e62b5创建于 1月15日历史提交
// 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.

#include "ui/views/interaction/interaction_test_util_views.h"

#include <string>
#include <utility>

#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/interaction_test_util.h"
#include "ui/base/test/ui_controls.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/gesture_event_details.h"
#include "ui/events/types/event_type.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/combobox/combobox.h"
#include "ui/views/controls/editable_combobox/editable_combobox.h"
#include "ui/views/controls/menu/menu_host.h"
#include "ui/views/controls/menu/menu_host_root_view.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/controls/tabbed_pane/tabbed_pane.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/test/widget_activation_waiter.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view.h"
#include "ui/views/view_observer.h"
#include "ui/views/view_tracker.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/window/dialog_delegate.h"

#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif

#if BUILDFLAG(IS_LINUX) && BUILDFLAG(IS_OZONE) && !BUILDFLAG(IS_CHROMEOS)
#define HANDLE_WAYLAND_FAILURE 1
#else
#define HANDLE_WAYLAND_FAILURE 0
#endif

#if HANDLE_WAYLAND_FAILURE
#include "ui/views/widget/widget_observer.h"
#endif

namespace views::test {

namespace {

#if HANDLE_WAYLAND_FAILURE

// On Wayland on Linux, window activation isn't guaranteed to work (based on
// what compositor extensions are installed). So instead, make a best effort to
// wait for the widget to activate, and if it fails, skip the test as it cannot
// possibly pass.
class WidgetActivationWaiterWayland final : public WidgetObserver {
 public:
  // A more than reasonable amount of time to wait for a window to activate.
  static constexpr base::TimeDelta kTimeout = base::Seconds(1);

  // Constructs an activation waiter for the given widget.
  explicit WidgetActivationWaiterWayland(Widget* widget)
      : active_(widget->IsActive()) {
    if (!active_) {
      widget_observation_.Observe(widget);
    }
  }
  ~WidgetActivationWaiterWayland() override = default;

  // Waits for the widget to become active or the operation to time out; returns
  // true on success. Returns immediately if the widget is already active.
  bool Wait() {
    if (!active_) {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&WidgetActivationWaiterWayland::OnTimeout,
                         weak_ptr_factory_.GetWeakPtr()),
          kTimeout);
      run_loop_.Run();
    }
    return active_;
  }

 private:
  // WidgetObserver:
  void OnWidgetDestroyed(Widget* widget) override {
    NOTREACHED() << "Widget destroyed before observation.";
  }
  void OnWidgetActivationChanged(Widget* widget, bool active) override {
    if (!active) {
      return;
    }
    active_ = true;
    widget_observation_.Reset();
    run_loop_.Quit();
  }

  void OnTimeout() {
    widget_observation_.Reset();
    run_loop_.Quit();
  }

  bool active_;
  base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed};
  base::ScopedObservation<Widget, WidgetObserver> widget_observation_{this};
  base::WeakPtrFactory<WidgetActivationWaiterWayland> weak_ptr_factory_{this};
};

#endif  // HANDLE_WAYLAND_FAILURE

// Waits for the dropdown pop-up and selects the specified item from the list.
class DropdownItemSelector {
 public:
  // The owning `simulator` will be used to simulate a click on the
  // `item_index`-th drop-down menu item using `input_type`.
  DropdownItemSelector(InteractionTestUtilSimulatorViews* simulator,
                       ui::test::InteractionTestUtil::InputType input_type,
                       size_t item_index)
      : simulator_(simulator),
        input_type_(input_type),
        item_index_(item_index) {
    observer_.set_shown_callback(base::BindRepeating(
        &DropdownItemSelector::OnWidgetShown, weak_ptr_factory_.GetWeakPtr()));
    observer_.set_hidden_callback(base::BindRepeating(
        &DropdownItemSelector::OnWidgetHidden, weak_ptr_factory_.GetWeakPtr()));
  }
  DropdownItemSelector(const DropdownItemSelector&) = delete;
  void operator=(const DropdownItemSelector&) = delete;
  ~DropdownItemSelector() = default;

  // Synchronously waits for the drop-down to appear and selects the appropriate
  // item.
  void SelectItem() {
    CHECK(!run_loop_.running());
    CHECK(!result_.has_value());
    run_loop_.Run();
  }

  // Returns whether the operation succeeded or failed.
  ui::test::ActionResult result() const {
    return result_.value_or(ui::test::ActionResult::kFailed);
  }

 private:
  // Responds to a new widget being shown. The assumption is that this widget is
  // the combobox dropdown. If it is not, the follow-up call will fail.
  void OnWidgetShown(Widget* widget) {
    if (widget_ || result_.has_value()) {
      return;
    }

    widget_ = widget;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&DropdownItemSelector::SelectItemImpl,
                                  weak_ptr_factory_.GetWeakPtr()));
  }

  // Detects when a widget is hidden. Fails the operation if this was the drop-
  // down widget and the item has not yet been selected.
  void OnWidgetHidden(Widget* widget) {
    if (result_.has_value() || widget_ != widget) {
      return;
    }

    LOG(ERROR) << "Widget closed before selection took place.";
    SetResult(ui::test::ActionResult::kFailed);
  }

  // Actually finds and selects the item in the drop-down. If it is not present
  // or cannot be selected, fails the operation.
  void SelectItemImpl() {
    CHECK(widget_);
    CHECK(!result_.has_value());

    // Because this widget was just shown, it may not be laid out yet.
    widget_->LayoutRootViewIfNecessary();
    size_t index = item_index_;
    if (auto* const menu_item =
            FindMenuItem(widget_->GetContentsView(), index)) {
      // No longer tracking the widget. This will prevent synchronous widget
      // dismissed during SelectMenuItem() below from thinking it failed.
      widget_ = nullptr;

      // Try to select the item.
      const auto result = simulator_->SelectMenuItem(
          ElementTrackerViews::GetInstance()->GetElementForView(menu_item,
                                                                true),
          input_type_);
      SetResult(result);
      switch (result) {
        case ui::test::ActionResult::kFailed:
          LOG(ERROR) << "Unable to select dropdown menu item.";
          break;
        case ui::test::ActionResult::kNotAttempted:
          NOTREACHED();
        case ui::test::ActionResult::kKnownIncompatible:
          LOG(WARNING)
              << "Select dropdown item not available on this platform with "
                 "input type "
              << input_type_;
          break;
        case ui::test::ActionResult::kSucceeded:
          break;
      }
    } else {
      LOG(ERROR) << "Dropdown menu item not found.";
      SetResult(ui::test::ActionResult::kFailed);
    }
  }

  // Sets the result and aborts `run_loop_`. Should only ever be called once.
  void SetResult(ui::test::ActionResult result) {
    CHECK(!result_.has_value());
    result_ = result;
    widget_ = nullptr;
    weak_ptr_factory_.InvalidateWeakPtrs();
    run_loop_.Quit();
  }

  // Recursively search `from` for the `index`-th MenuItemView.
  //
  // Searches in-order, depth-first. It is assumed that menu items will appear
  // in search order in the same order they appear visually.
  static MenuItemView* FindMenuItem(View* from, size_t& index) {
    for (views::View* child : from->children()) {
      auto* const item = AsViewClass<MenuItemView>(child);
      if (item) {
        if (index == 0U) {
          return item;
        }
        --index;
      } else if (auto* result = FindMenuItem(child, index)) {
        return result;
      }
    }
    return nullptr;
  }

  const raw_ptr<InteractionTestUtilSimulatorViews> simulator_;
  const ui::test::InteractionTestUtil::InputType input_type_;
  const size_t item_index_;
  base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed};
  AnyWidgetObserver observer_{views::test::AnyWidgetTestPasskey()};  // IN-TEST
  std::optional<ui::test::ActionResult> result_;
  raw_ptr<Widget> widget_ = nullptr;
  base::WeakPtrFactory<DropdownItemSelector> weak_ptr_factory_{this};
};

gfx::Point GetCenter(views::View* view) {
  return view->GetLocalBounds().CenterPoint();
}

bool SendDefaultAction(View* target) {
  ui::AXActionData action;
  action.action = ax::mojom::Action::kDoDefault;
  return target->HandleAccessibleAction(action);
}

// Sends a mouse click to the specified `target`.
// Views are EventHandlers but Widgets are not despite having the same API for
// event handling, so use a templated approach to support both cases.
template <class T>
void SendMouseClick(T* target, const gfx::Point& point) {
  ui::MouseEvent mouse_down(ui::EventType::kMousePressed, point, point,
                            ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
                            ui::EF_LEFT_MOUSE_BUTTON);
  target->OnMouseEvent(&mouse_down);
  ui::MouseEvent mouse_up(ui::EventType::kMouseReleased, point, point,
                          ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
                          ui::EF_LEFT_MOUSE_BUTTON);
  target->OnMouseEvent(&mouse_up);
}

// Sends a tap gesture to the specified `target`.
// Views are EventHandlers but Widgets are not despite having the same API for
// event handling, so use a templated approach to support both cases.
template <class T>
void SendTapGesture(T* target, const gfx::Point& point) {
  ui::GestureEventDetails press_details(ui::EventType::kGestureTap);
  press_details.set_device_type(ui::GestureDeviceType::DEVICE_TOUCHSCREEN);
  ui::GestureEvent press_event(point.x(), point.y(), ui::EF_NONE,
                               ui::EventTimeForNow(), press_details);
  target->OnGestureEvent(&press_event);

  ui::GestureEventDetails release_details(ui::EventType::kGestureEnd);
  release_details.set_device_type(ui::GestureDeviceType::DEVICE_TOUCHSCREEN);
  ui::GestureEvent release_event(point.x(), point.y(), ui::EF_NONE,
                                 ui::EventTimeForNow(), release_details);
  target->OnGestureEvent(&release_event);
}

// Sends a key press to the specified `target`. Returns true if the view is
// still valid after processing the keypress.
bool SendKeyPress(View* view, ui::KeyboardCode code, int flags = ui::EF_NONE) {
  ViewTracker tracker(view);
  view->OnKeyPressed(ui::KeyEvent(ui::EventType::kKeyPressed, code, flags,
                                  ui::EventTimeForNow()));

  // Verify that the button is not destroyed after the key-down before trying
  // to send the key-up.
  if (!tracker.view()) {
    return false;
  }

  tracker.view()->OnKeyReleased(ui::KeyEvent(ui::EventType::kKeyReleased, code,
                                             flags, ui::EventTimeForNow()));

  return tracker.view();
}

}  // namespace

ViewFocusedWaiter::ViewFocusedWaiter(View& target_view)
    : manager_(*target_view.GetFocusManager()), target_view_(target_view) {
  manager_->AddFocusChangeListener(this);
}

ViewFocusedWaiter::~ViewFocusedWaiter() {
  manager_->RemoveFocusChangeListener(this);
}

void ViewFocusedWaiter::Wait() {
  if (manager_->GetFocusedView() != &*target_view_) {
    run_loop_.Run();
  }
}

void ViewFocusedWaiter::OnWillChangeFocus(View* focused_before,
                                          View* focused_now) {}
void ViewFocusedWaiter::OnDidChangeFocus(View* focused_before,
                                         View* focused_now) {
  if (focused_now == &*target_view_) {
    run_loop_.Quit();
  }
}

InteractionTestUtilSimulatorViews::InteractionTestUtilSimulatorViews() =
    default;
InteractionTestUtilSimulatorViews::~InteractionTestUtilSimulatorViews() =
    default;

ui::test::ActionResult InteractionTestUtilSimulatorViews::PressButton(
    ui::TrackedElement* element,
    InputType input_type) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }
  auto* const button =
      Button::AsButton(element->AsA<TrackedElementViews>()->view());
  if (!button) {
    return ui::test::ActionResult::kNotAttempted;
  }

  PressButton(button, input_type);
  return ui::test::ActionResult::kSucceeded;
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::SelectMenuItem(
    ui::TrackedElement* element,
    InputType input_type) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }
  auto* const menu_item =
      AsViewClass<MenuItemView>(element->AsA<TrackedElementViews>()->view());
  if (!menu_item) {
    return ui::test::ActionResult::kNotAttempted;
  }

#if BUILDFLAG(IS_MAC)
  // Keyboard input isn't reliable on Mac for submenus, so unless the test
  // specifically calls for keyboard input, prefer mouse.
  if (input_type == ui::test::InteractionTestUtil::InputType::kDontCare) {
    input_type = ui::test::InteractionTestUtil::InputType::kMouse;
  }
#endif  // BUILDFLAG(IS_MAC)

  auto* const host = menu_item->GetWidget()->GetRootView();
  gfx::Point point = GetCenter(menu_item);
  View::ConvertPointToTarget(menu_item, host, &point);

  switch (input_type) {
    case ui::test::InteractionTestUtil::InputType::kMouse:
      SendMouseClick(host->GetWidget(), point);
      break;
    case ui::test::InteractionTestUtil::InputType::kTouch:
      SendTapGesture(host->GetWidget(), point);
      break;
    case ui::test::InteractionTestUtil::InputType::kKeyboard:
    case ui::test::InteractionTestUtil::InputType::kDontCare: {
#if BUILDFLAG(IS_MAC)
      constexpr ui::KeyboardCode kSelectMenuKeyboardCode = ui::VKEY_SPACE;
#else
      constexpr ui::KeyboardCode kSelectMenuKeyboardCode = ui::VKEY_RETURN;
#endif
      MenuController* const controller = menu_item->GetMenuController();
      controller->SelectItemAndOpenSubmenu(menu_item);
      ui::KeyEvent key_event(ui::EventType::kKeyPressed,
                             kSelectMenuKeyboardCode, ui::EF_NONE,
                             ui::EventTimeForNow());
      controller->OnWillDispatchKeyEvent(&key_event);
      break;
    }
  }
  return ui::test::ActionResult::kSucceeded;
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::DoDefaultAction(
    ui::TrackedElement* element,
    InputType input_type) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }
  if (!DoDefaultAction(element->AsA<TrackedElementViews>()->view(),
                       input_type)) {
    LOG(ERROR) << "Failed to send default action to " << *element;
    return ui::test::ActionResult::kFailed;
  }
  return ui::test::ActionResult::kSucceeded;
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::SelectTab(
    ui::TrackedElement* tab_collection,
    size_t index,
    InputType input_type,
    std::optional<size_t> expected_index_after_selection) {
  // Currently, only TabbedPane is supported, but other types of tab
  // collections (e.g. browsers and tabstrips) may be supported by a different
  // kind of simulator specific to browser code, so if this is not a supported
  // View type, just return false instead of sending an error.
  if (!tab_collection->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }
  auto* const pane = views::AsViewClass<TabbedPane>(
      tab_collection->AsA<TrackedElementViews>()->view());
  if (!pane) {
    return ui::test::ActionResult::kNotAttempted;
  }

  // Unlike with the element type, an out-of-bounds tab is always an error.
  auto* const tab = pane->GetTabAt(index);
  if (!tab) {
    LOG(ERROR) << "Tab index " << index << " out of range, there are "
               << pane->GetTabCount() << " tabs.";
    return ui::test::ActionResult::kFailed;
  }
  switch (input_type) {
    case ui::test::InteractionTestUtil::InputType::kDontCare:
      if (!SendDefaultAction(tab)) {
        LOG(ERROR) << "Failed to send default action to tab.";
        return ui::test::ActionResult::kFailed;
      }
      break;
    case ui::test::InteractionTestUtil::InputType::kMouse:
      SendMouseClick(tab, GetCenter(tab));
      break;
    case ui::test::InteractionTestUtil::InputType::kTouch:
      SendTapGesture(tab, GetCenter(tab));
      break;
    case ui::test::InteractionTestUtil::InputType::kKeyboard: {
      CHECK_EQ(index, expected_index_after_selection.value_or(index))
          << "Since keyboard input requires advancing through tabs one-by-one, "
             "a different expected index cannot be used.";
      // Keyboard navigation is done by sending arrow keys to the currently-
      // selected tab. Scan through the tabs by using the right arrow until the
      // correct tab is selected; limit the number of times this is tried to
      // avoid in infinite loop if something goes wrong.
      const auto current_index = pane->GetSelectedTabIndex();
      if (current_index != index) {
        const auto code = ((current_index > index) ^ base::i18n::IsRTL())
                              ? ui::VKEY_LEFT
                              : ui::VKEY_RIGHT;
        const int count =
            std::abs(static_cast<int>(index) - static_cast<int>(current_index));
        LOG_IF(WARNING, count > 1)
            << "SelectTab via keyboard from " << current_index << " to "
            << index << " will pass through intermediate tabs.";
        for (int i = 0; i < count; ++i) {
          auto* const current_tab = pane->GetTabAt(pane->GetSelectedTabIndex());
          ::views::test::SendKeyPress(current_tab, code);
        }
        if (index != pane->GetSelectedTabIndex()) {
          LOG(ERROR) << "Unable to cycle through tabs to reach index " << index;
          return ui::test::ActionResult::kFailed;
        }
      }
      break;
    }
  }
  const size_t expected = expected_index_after_selection.value_or(index);
  const size_t actual = pane->GetSelectedTabIndex();
  if (actual != expected) {
    LOG(ERROR) << "Expected to finish with index " << expected
               << " but selected index was " << actual;
    return ui::test::ActionResult::kFailed;
  }
  return ui::test::ActionResult::kSucceeded;
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::SelectDropdownItem(
    ui::TrackedElement* dropdown,
    size_t index,
    InputType input_type) {
  if (!dropdown->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }
  auto* const view = dropdown->AsA<TrackedElementViews>()->view();
  auto* const combobox = views::AsViewClass<Combobox>(view);
  auto* const editable_combobox = views::AsViewClass<EditableCombobox>(view);
  if (!combobox && !editable_combobox) {
    return ui::test::ActionResult::kNotAttempted;
  }
  auto* const model =
      combobox ? combobox->GetModel() : editable_combobox->GetComboboxModel();
  if (index >= model->GetItemCount()) {
    LOG(ERROR) << "Item index " << index << " is out of range, there are "
               << model->GetItemCount() << " items.";
    return ui::test::ActionResult::kFailed;
  }

  // InputType::kDontCare is implemented in a way that is safe across all
  // platforms and most test environments; it does not rely on popping up the
  // dropdown and selecting individual items.
  if (input_type == InputType::kDontCare) {
    if (combobox) {
      combobox->MenuSelectionAt(index);
    } else {
      editable_combobox->SetText(model->GetItemAt(index));
    }
    return ui::test::ActionResult::kSucceeded;
  }

  // For specific input types, the dropdown will be popped out. Because of
  // asynchronous and event-handling issues, this is not yet supported on Mac.
#if BUILDFLAG(IS_MAC)
  LOG(WARNING) << "SelectDropdownItem(): "
                  "only InputType::kDontCare is supported on Mac.";
  return ui::test::ActionResult::kKnownIncompatible;
#else

  // This is required in case we want to repeatedly test a combobox; otherwise
  // it will refuse to open the second time.
  if (combobox) {
    combobox->closed_time_ = base::TimeTicks();
  }

  // The highest-fidelity input simulation involves actually opening the
  // drop-down and selecting an item from the list.
  DropdownItemSelector selector(this, input_type, index);

  // Try to get the arrow. If it's present, the combobox will be opened by
  // activating the button. Note that while Combobox has the ability to hide its
  // arrow, the button is still present and visible, just transparent.
  auto* const arrow = combobox ? combobox->arrow_button_.get()
                               : editable_combobox->arrow_.get();
  if (arrow) {
    PressButton(arrow, input_type);
  } else {
    if (!editable_combobox) {
      LOG(ERROR) << "Only EditableCombobox should have the option to "
                    "completely remove its arrow.";
      return ui::test::ActionResult::kFailed;
    }
    // Editable comboboxes without visible arrows exist, but are weird.
    switch (input_type) {
      case InputType::kDontCare:
      case InputType::kKeyboard:
        // Have to resort to keyboard input; DoDefaultAction() doesn't work.
        ::views::test::SendKeyPress(editable_combobox->textfield_,
                                    ui::VKEY_DOWN);
        break;
      default:
        LOG(WARNING) << "Mouse and touch input are not supported for "
                        "comboboxes without visible arrows.";
        return ui::test::ActionResult::kNotAttempted;
    }
  }

  selector.SelectItem();
  return selector.result();
#endif
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::EnterText(
    ui::TrackedElement* element,
    std::u16string text,
    TextEntryMode mode) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }
  auto* const view = element->AsA<TrackedElementViews>()->view();

  // Currently, Textfields (and derived types like Textareas) are supported, as
  // well as EditableCombobox.
  Textfield* textfield = AsViewClass<Textfield>(view);
  if (!textfield && IsViewClass<EditableCombobox>(view)) {
    textfield = AsViewClass<EditableCombobox>(view)->textfield_;
  }

  if (!textfield) {
    return ui::test::ActionResult::kNotAttempted;
  }

  if (textfield->GetReadOnly()) {
    LOG(ERROR) << "Cannot set text on read-only textfield.";
    return ui::test::ActionResult::kFailed;
  }

  // Textfield does not expose all of the power of RenderText so some care has
  // to be taken in positioning the input caret using the methods available to
  // this class.
  switch (mode) {
    case TextEntryMode::kAppend: {
      // Determine the start and end of the selectable range, and position the
      // caret immediately after the end of the range. This approach does not
      // make any assumptions about the indexing mode of the text,
      // multi-codepoint characters, etc.
      textfield->SelectAll(false);
      auto range = textfield->GetSelectedRange();
      range.set_start(range.end());
      textfield->SetSelectedRange(range);
      break;
    }
    case TextEntryMode::kInsertOrReplace:
      // No action needed; keep selection and cursor as they are.
      break;
    case TextEntryMode::kReplaceAll:
      textfield->SelectAll(false);
      break;
  }

  // This is an IME method that is the closest thing to inserting text from
  // the user rather than setting it programmatically.
  textfield->InsertText(
      text,
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  return ui::test::ActionResult::kSucceeded;
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::ActivateSurface(
    ui::TrackedElement* element) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }

  auto* const widget = element->AsA<TrackedElementViews>()->view()->GetWidget();
  if (!widget) {
    LOG(WARNING) << "View not associated with a widget.";
    return ui::test::ActionResult::kFailed;
  }

  return ActivateWidget(widget);
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::FocusElement(
    ui::TrackedElement* element) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }

  auto* const view = element->AsA<TrackedElementViews>()->view();
  if (!view->GetWidget()) {
    LOG(WARNING) << "View not associated with a widget.";
    return ui::test::ActionResult::kFailed;
  }

  // Note: this duplicates logic in View that is not public.
  if (!view->IsFocusable()) {
    if (!view->GetViewAccessibility().IsAccessibilityFocusable()) {
      LOG(WARNING) << "View cannot request focus.";
      return ui::test::ActionResult::kFailed;
    }
    LOG(WARNING) << "Switching focus manager to accessibility mode.";
    view->GetFocusManager()->SetKeyboardAccessible(true);
  }

#if BUILDFLAG(IS_MAC)
  if (!view->GetWidget()->IsActive()) {
    LOG(WARNING) << "View cannot receive focus in inactive window on this "
                    "platform; activate this surface first..";
    return ui::test::ActionResult::kFailed;
  }
#endif

  ViewFocusedWaiter waiter(*view);
  view->RequestFocus();
  waiter.Wait();
  return ui::test::ActionResult::kSucceeded;
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::SendAccelerator(
    ui::TrackedElement* element,
    ui::Accelerator accelerator) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }

  element->AsA<TrackedElementViews>()
      ->view()
      ->GetFocusManager()
      ->ProcessAccelerator(accelerator);
  return ui::test::ActionResult::kSucceeded;
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::SendKeyPress(
    ui::TrackedElement* element,
    ui::KeyboardCode key,
    int flags) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }

  base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);

  const auto native_window = element->AsA<TrackedElementViews>()
                                 ->view()
                                 ->GetWidget()
                                 ->GetNativeWindow();
  const bool result = ui_controls::SendKeyPressNotifyWhenDone(
      native_window, key, flags & ui::EF_CONTROL_DOWN,
      flags & ui::EF_SHIFT_DOWN, flags & ui::EF_ALT_DOWN,
      flags & ui::EF_COMMAND_DOWN, run_loop.QuitClosure());
  if (!result) {
    LOG(ERROR) << "Send key press failed. Is this the active window?";
    return ui::test::ActionResult::kFailed;
  }
  run_loop.Run();
  return ui::test::ActionResult::kSucceeded;
}

ui::test::ActionResult InteractionTestUtilSimulatorViews::Confirm(
    ui::TrackedElement* element) {
  if (!element->IsA<TrackedElementViews>()) {
    return ui::test::ActionResult::kNotAttempted;
  }
  auto* const view = element->AsA<TrackedElementViews>()->view();

  // Currently, only dialogs can be confirmed. Fetch the delegate and call
  // Accept().
  DialogDelegate* delegate = nullptr;
  if (auto* const dialog = AsViewClass<DialogDelegateView>(view)) {
    delegate = dialog->AsDialogDelegate();
  } else if (auto* const bubble = AsViewClass<BubbleDialogDelegateView>(view)) {
    delegate = bubble->AsDialogDelegate();
  }

  if (!delegate) {
    return ui::test::ActionResult::kNotAttempted;
  }

  if (!delegate->GetOkButton()) {
    LOG(ERROR) << "Confirm(): cannot confirm dialog that has no OK button.";
    return ui::test::ActionResult::kFailed;
  }

  delegate->AcceptDialog();
  return ui::test::ActionResult::kSucceeded;
}

// static
bool InteractionTestUtilSimulatorViews::IsWayland() {
#if BUILDFLAG(IS_OZONE)
  return ui::OzonePlatform::GetPlatformNameForTest() == "wayland";
#else
  return false;
#endif
}

// static
ui::test::ActionResult InteractionTestUtilSimulatorViews::ActivateWidget(
    Widget* widget) {
#if HANDLE_WAYLAND_FAILURE
  if (IsWayland()) {
    WidgetActivationWaiterWayland waiter(widget);
    widget->Activate();
    if (!waiter.Wait()) {
      LOG(WARNING)
          << "Unable to activate widget due to lack of Wayland support for "
             "widget activation; test is not meaningful on this platform.";
      return ui::test::ActionResult::kKnownIncompatible;
    }
    return ui::test::ActionResult::kSucceeded;
  }
#endif  // HANDLE_WAYLAND_FAILURE

  widget->Activate();
  views::test::WaitForWidgetActive(widget, true);
  return ui::test::ActionResult::kSucceeded;
}

// static
bool InteractionTestUtilSimulatorViews::DoDefaultAction(View* view,
                                                        InputType input_type) {
  switch (input_type) {
    case ui::test::InteractionTestUtil::InputType::kDontCare:
      return SendDefaultAction(view);
    case ui::test::InteractionTestUtil::InputType::kMouse:
      SendMouseClick(view, GetCenter(view));
      return true;
    case ui::test::InteractionTestUtil::InputType::kTouch:
      SendTapGesture(view, GetCenter(view));
      return true;
    case ui::test::InteractionTestUtil::InputType::kKeyboard:
      ::views::test::SendKeyPress(view, ui::VKEY_SPACE);
      return true;
  }
}

// static
void InteractionTestUtilSimulatorViews::PressButton(Button* button,
                                                    InputType input_type) {
  switch (input_type) {
    case ui::test::InteractionTestUtil::InputType::kMouse:
      SendMouseClick(button, GetCenter(button));
      break;
    case ui::test::InteractionTestUtil::InputType::kTouch:
      SendTapGesture(button, GetCenter(button));
      break;
    case ui::test::InteractionTestUtil::InputType::kKeyboard:
    case ui::test::InteractionTestUtil::InputType::kDontCare:
      ::views::test::SendKeyPress(button, ui::VKEY_SPACE);
      break;
  }
}

}  // namespace views::test