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