blob: 468fcc9a7e9a08abe9e0b458331c8efb4b9c3f26 [file] [log] [blame]
// Copyright 2013 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/widget/root_view.h"
#include <memory>
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/menu_source_type.mojom.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/context_menu_controller.h"
#include "ui/views/test/ax_event_counter.h"
#include "ui/views/test/test_views.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/view_targeter.h"
#include "ui/views/widget/widget_deletion_observer.h"
#include "ui/views/window/dialog_delegate.h"
#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#include "base/test/scoped_feature_list.h"
#include "ui/accessibility/accessibility_features.h"
#endif
namespace views::test {
namespace {
struct RootViewTestStateInit {
gfx::Rect bounds;
Widget::InitParams::Type type = Widget::InitParams::TYPE_WINDOW_FRAMELESS;
};
class RootViewTestState {
public:
explicit RootViewTestState(ViewsTestBase* delegate,
RootViewTestStateInit init = {}) {
Widget::InitParams init_params = delegate->CreateParams(
Widget::InitParams::CLIENT_OWNS_WIDGET, init.type);
if (init.bounds != gfx::Rect()) {
init_params.bounds = init.bounds;
}
widget_.Init(std::move(init_params));
widget_.Show();
widget_.SetContentsView(std::make_unique<View>());
}
Widget* widget() { return &widget_; }
internal::RootView* GetRootView() {
return static_cast<internal::RootView*>(widget_.GetRootView());
}
template <typename T>
T* AddChildView(std::unique_ptr<T> view) {
return widget_.GetContentsView()->AddChildView(std::move(view));
}
private:
Widget widget_;
};
class DeleteOnKeyEventView : public View {
METADATA_HEADER(DeleteOnKeyEventView, View)
public:
explicit DeleteOnKeyEventView(bool* set_on_key) : set_on_key_(set_on_key) {}
DeleteOnKeyEventView(const DeleteOnKeyEventView&) = delete;
DeleteOnKeyEventView& operator=(const DeleteOnKeyEventView&) = delete;
~DeleteOnKeyEventView() override = default;
bool OnKeyPressed(const ui::KeyEvent& event) override {
*set_on_key_ = true;
delete this;
return true;
}
private:
// Set to true in OnKeyPressed().
raw_ptr<bool> set_on_key_;
};
BEGIN_METADATA(DeleteOnKeyEventView)
END_METADATA
} // namespace
using RootViewTest = ViewsTestBase;
// Verifies deleting a View in OnKeyPressed() doesn't crash and that the
// target is marked as destroyed in the returned EventDispatchDetails.
TEST_F(RootViewTest, DeleteViewDuringKeyEventDispatch) {
RootViewTestState state(this);
internal::RootView* root_view = state.GetRootView();
bool got_key_event = false;
View* child = state.AddChildView(
std::make_unique<DeleteOnKeyEventView>(&got_key_event));
// Give focus to |child| so that it will be the target of the key event.
child->SetFocusBehavior(View::FocusBehavior::ALWAYS);
child->RequestFocus();
ViewTargeter* view_targeter = new ViewTargeter(root_view);
root_view->SetEventTargeter(base::WrapUnique(view_targeter));
ui::KeyEvent key_event(ui::EventType::kKeyPressed, ui::VKEY_ESCAPE,
ui::EF_NONE);
ui::EventDispatchDetails details = root_view->OnEventFromSource(&key_event);
EXPECT_TRUE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_TRUE(got_key_event);
}
// Tracks whether a context menu is shown.
class TestContextMenuController : public ContextMenuController {
public:
TestContextMenuController() = default;
TestContextMenuController(const TestContextMenuController&) = delete;
TestContextMenuController& operator=(const TestContextMenuController&) =
delete;
~TestContextMenuController() override = default;
int show_context_menu_calls() const { return show_context_menu_calls_; }
View* menu_source_view() const { return menu_source_view_; }
ui::mojom::MenuSourceType menu_source_type() const {
return menu_source_type_;
}
void Reset() {
show_context_menu_calls_ = 0;
menu_source_view_ = nullptr;
menu_source_type_ = ui::mojom::MenuSourceType::kNone;
}
// ContextMenuController:
void ShowContextMenuForViewImpl(
View* source,
const gfx::Point& point,
ui::mojom::MenuSourceType source_type) override {
show_context_menu_calls_++;
menu_source_view_ = source;
menu_source_type_ = source_type;
}
private:
int show_context_menu_calls_ = 0;
raw_ptr<View> menu_source_view_ = nullptr;
ui::mojom::MenuSourceType menu_source_type_ =
ui::mojom::MenuSourceType::kNone;
};
// Tests that context menus are shown for certain key events (Shift+F10
// and VKEY_APPS) by the pre-target handler installed on RootView.
TEST_F(RootViewTest, ContextMenuFromKeyEvent) {
// This behavior is intentionally unsupported on macOS.
#if !BUILDFLAG(IS_MAC)
RootViewTestState state(this);
internal::RootView* root_view = state.GetRootView();
TestContextMenuController controller;
View* focused_view = root_view->GetContentsView();
focused_view->set_context_menu_controller(&controller);
focused_view->SetFocusBehavior(View::FocusBehavior::ALWAYS);
focused_view->RequestFocus();
// No context menu should be shown for a keypress of 'A'.
ui::KeyEvent nomenu_key_event = ui::KeyEvent::FromCharacter(
'a', ui::VKEY_A, ui::DomCode::NONE, ui::EF_NONE);
ui::EventDispatchDetails details =
root_view->OnEventFromSource(&nomenu_key_event);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(0, controller.show_context_menu_calls());
EXPECT_EQ(nullptr, controller.menu_source_view());
EXPECT_EQ(ui::mojom::MenuSourceType::kNone, controller.menu_source_type());
controller.Reset();
// A context menu should be shown for a keypress of Shift+F10.
ui::KeyEvent menu_key_event(ui::EventType::kKeyPressed, ui::VKEY_F10,
ui::EF_SHIFT_DOWN);
details = root_view->OnEventFromSource(&menu_key_event);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(1, controller.show_context_menu_calls());
EXPECT_EQ(focused_view, controller.menu_source_view());
EXPECT_EQ(ui::mojom::MenuSourceType::kKeyboard,
controller.menu_source_type());
controller.Reset();
// A context menu should be shown for a keypress of VKEY_APPS.
ui::KeyEvent menu_key_event2(ui::EventType::kKeyPressed, ui::VKEY_APPS,
ui::EF_NONE);
details = root_view->OnEventFromSource(&menu_key_event2);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(1, controller.show_context_menu_calls());
EXPECT_EQ(focused_view, controller.menu_source_view());
EXPECT_EQ(ui::mojom::MenuSourceType::kKeyboard,
controller.menu_source_type());
controller.Reset();
#endif
}
// View which handles all gesture events.
class GestureHandlingView : public View {
METADATA_HEADER(GestureHandlingView, View)
public:
GestureHandlingView() = default;
GestureHandlingView(const GestureHandlingView&) = delete;
GestureHandlingView& operator=(const GestureHandlingView&) = delete;
~GestureHandlingView() override = default;
void OnGestureEvent(ui::GestureEvent* event) override { event->SetHandled(); }
};
BEGIN_METADATA(GestureHandlingView)
END_METADATA
// View which handles all mouse events.
class MouseHandlingView : public View {
METADATA_HEADER(MouseHandlingView, View)
public:
MouseHandlingView() = default;
MouseHandlingView(const MouseHandlingView&) = delete;
MouseHandlingView& operator=(const MouseHandlingView&) = delete;
~MouseHandlingView() override = default;
// View:
void OnMouseEvent(ui::MouseEvent* event) override { event->SetHandled(); }
};
BEGIN_METADATA(MouseHandlingView)
END_METADATA
TEST_F(RootViewTest, EventHandlersResetWhenDeleted) {
RootViewTestState state(this, {.bounds = {100, 100}});
internal::RootView* root_view = state.GetRootView();
// Set up a child view to handle events.
View* event_handler = state.AddChildView(std::make_unique<View>());
root_view->SetMouseAndGestureHandler(event_handler);
ASSERT_EQ(event_handler, root_view->gesture_handler_for_testing());
ASSERT_EQ(event_handler, root_view->mouse_pressed_handler_for_testing());
// Delete the child and expect that there is no longer a mouse handler.
root_view->GetContentsView()->RemoveChildViewT(event_handler);
EXPECT_EQ(nullptr, root_view->gesture_handler_for_testing());
EXPECT_EQ(nullptr, root_view->mouse_pressed_handler_for_testing());
}
TEST_F(RootViewTest, EventHandlersNotResetWhenReparented) {
RootViewTestState state(this, {.bounds = {100, 100}});
internal::RootView* root_view = state.GetRootView();
// Set up a child view to handle events
View* event_handler = state.AddChildView(std::make_unique<View>());
root_view->SetMouseAndGestureHandler(event_handler);
ASSERT_EQ(event_handler, root_view->gesture_handler_for_testing());
// Reparent the child within the hierarchy and expect that it's still the
// mouse handler.
View* other_parent = state.AddChildView(std::make_unique<View>());
other_parent->AddChildViewRaw(event_handler);
EXPECT_EQ(event_handler, root_view->gesture_handler_for_testing());
}
// Verifies that the gesture handler stored in the root view is reset after
// mouse is released. Note that during mouse event handling,
// `RootView::SetMouseAndGestureHandler()` may be called to set the gesture
// handler. Therefore we should reset the gesture handler when mouse is
// released. We may remove this test in the future if the implementation of the
// product code changes.
TEST_F(RootViewTest, GestureHandlerResetAfterMouseReleased) {
RootViewTestState state(this, {.bounds = {100, 100}});
internal::RootView* root_view = state.GetRootView();
// Create a child view to handle gestures.
View* gesture_handler =
state.AddChildView(std::make_unique<GestureHandlingView>());
gesture_handler->SetBoundsRect(gfx::Rect(gfx::Size{50, 50}));
// Create a child view to handle mouse events.
View* mouse_handler =
state.AddChildView(std::make_unique<MouseHandlingView>());
mouse_handler->SetBoundsRect(
gfx::Rect(gesture_handler->bounds().bottom_right(), gfx::Size{50, 50}));
// Emulate to start gesture scroll on `child_view`.
const gfx::Point gesture_handler_center_point =
gesture_handler->GetBoundsInScreen().CenterPoint();
ui::GestureEvent scroll_begin(
gesture_handler_center_point.x(), gesture_handler_center_point.y(),
ui::EF_NONE, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
root_view->OnEventFromSource(&scroll_begin);
ui::GestureEvent scroll_update(
gesture_handler_center_point.x(), gesture_handler_center_point.y(),
ui::EF_NONE, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate,
/*delta_x=*/20,
/*delta_y=*/10));
root_view->OnEventFromSource(&scroll_update);
// Emulate the mouse click on `mouse_handler` before gesture scroll ends.
const gfx::Point mouse_handler_center_point =
mouse_handler->GetBoundsInScreen().CenterPoint();
ui::MouseEvent pressed_event(
ui::EventType::kMousePressed, mouse_handler_center_point,
mouse_handler_center_point, ui::EventTimeForNow(), ui::EF_NONE,
/*changed_button_flags=*/0);
ui::MouseEvent released_event(
ui::EventType::kMouseReleased, mouse_handler_center_point,
mouse_handler_center_point, ui::EventTimeForNow(), ui::EF_NONE,
/*changed_button_flags=*/0);
root_view->OnMousePressed(pressed_event);
root_view->OnMouseReleased(released_event);
// Check that the gesture handler is reset.
EXPECT_EQ(nullptr, root_view->gesture_handler_for_testing());
}
// Tests that context menus are shown for long press by the post-target handler
// installed on the RootView only if the event is targetted at a view which can
// show a context menu.
TEST_F(RootViewTest, ContextMenuFromLongPress) {
RootViewTestState state(
this, {.bounds = {100, 100}, .type = Widget::InitParams::TYPE_POPUP});
internal::RootView* root_view = state.GetRootView();
View* parent_view = root_view->GetContentsView();
// Create a view capable of showing the context menu with two children one of
// which handles all gesture events (e.g. a button).
TestContextMenuController controller;
parent_view->set_context_menu_controller(&controller);
View* gesture_handling_child_view = new GestureHandlingView;
gesture_handling_child_view->SetBoundsRect(gfx::Rect(10, 10));
parent_view->AddChildViewRaw(gesture_handling_child_view);
View* other_child_view = new View;
other_child_view->SetBoundsRect(gfx::Rect(20, 0, 10, 10));
parent_view->AddChildViewRaw(other_child_view);
// |parent_view| should not show a context menu as a result of a long press on
// |gesture_handling_child_view|.
ui::GestureEvent long_press1(
5, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureLongPress));
ui::EventDispatchDetails details = root_view->OnEventFromSource(&long_press1);
ui::GestureEvent end1(5, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
details = root_view->OnEventFromSource(&end1);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(0, controller.show_context_menu_calls());
controller.Reset();
// |parent_view| should show a context menu as a result of a long press on
// |other_child_view|.
ui::GestureEvent long_press2(
25, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureLongPress));
details = root_view->OnEventFromSource(&long_press2);
ui::GestureEvent end2(25, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
details = root_view->OnEventFromSource(&end2);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(1, controller.show_context_menu_calls());
controller.Reset();
// |parent_view| should show a context menu as a result of a long press on
// itself.
ui::GestureEvent long_press3(
50, 50, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureLongPress));
details = root_view->OnEventFromSource(&long_press3);
ui::GestureEvent end3(25, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
details = root_view->OnEventFromSource(&end3);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(1, controller.show_context_menu_calls());
}
// Tests that context menus are not shown for disabled views on a long press.
TEST_F(RootViewTest, ContextMenuFromLongPressOnDisabledView) {
RootViewTestState state(
this, {.bounds = {100, 100}, .type = Widget::InitParams::TYPE_POPUP});
internal::RootView* root_view = state.GetRootView();
View* parent_view = root_view->GetContentsView();
// Create a view capable of showing the context menu with two children one of
// which handles all gesture events (e.g. a button). Also mark this view
// as disabled.
TestContextMenuController controller;
parent_view->set_context_menu_controller(&controller);
parent_view->SetEnabled(false);
View* gesture_handling_child_view = new GestureHandlingView;
gesture_handling_child_view->SetBoundsRect(gfx::Rect(10, 10));
parent_view->AddChildViewRaw(gesture_handling_child_view);
View* other_child_view = new View;
other_child_view->SetBoundsRect(gfx::Rect(20, 0, 10, 10));
parent_view->AddChildViewRaw(other_child_view);
// |parent_view| should not show a context menu as a result of a long press on
// |gesture_handling_child_view|.
ui::GestureEvent long_press1(
5, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureLongPress));
ui::EventDispatchDetails details = root_view->OnEventFromSource(&long_press1);
ui::GestureEvent end1(5, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
details = root_view->OnEventFromSource(&end1);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(0, controller.show_context_menu_calls());
controller.Reset();
// |parent_view| should not show a context menu as a result of a long press on
// |other_child_view|.
ui::GestureEvent long_press2(
25, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureLongPress));
details = root_view->OnEventFromSource(&long_press2);
ui::GestureEvent end2(25, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
details = root_view->OnEventFromSource(&end2);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(0, controller.show_context_menu_calls());
controller.Reset();
// |parent_view| should not show a context menu as a result of a long press on
// itself.
ui::GestureEvent long_press3(
50, 50, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureLongPress));
details = root_view->OnEventFromSource(&long_press3);
ui::GestureEvent end3(25, 5, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::EventType::kGestureEnd));
details = root_view->OnEventFromSource(&end3);
EXPECT_FALSE(details.target_destroyed);
EXPECT_FALSE(details.dispatcher_destroyed);
EXPECT_EQ(0, controller.show_context_menu_calls());
}
namespace {
// View class which destroys itself when it gets an event of type
// |delete_event_type|.
class DeleteViewOnEvent : public View {
METADATA_HEADER(DeleteViewOnEvent, View)
public:
DeleteViewOnEvent(ui::EventType delete_event_type, bool* was_destroyed)
: delete_event_type_(delete_event_type), was_destroyed_(was_destroyed) {}
DeleteViewOnEvent(const DeleteViewOnEvent&) = delete;
DeleteViewOnEvent& operator=(const DeleteViewOnEvent&) = delete;
~DeleteViewOnEvent() override { *was_destroyed_ = true; }
void OnEvent(ui::Event* event) override {
if (event->type() == delete_event_type_) {
delete this;
}
}
private:
// The event type which causes the view to destroy itself.
ui::EventType delete_event_type_;
// Tracks whether the view was destroyed.
raw_ptr<bool> was_destroyed_;
};
BEGIN_METADATA(DeleteViewOnEvent)
END_METADATA
// View class which remove itself when it gets an event of type
// |remove_event_type|.
class RemoveViewOnEvent : public View {
METADATA_HEADER(RemoveViewOnEvent, View)
public:
explicit RemoveViewOnEvent(ui::EventType remove_event_type)
: remove_event_type_(remove_event_type) {}
RemoveViewOnEvent(const RemoveViewOnEvent&) = delete;
RemoveViewOnEvent& operator=(const RemoveViewOnEvent&) = delete;
void OnEvent(ui::Event* event) override {
if (event->type() == remove_event_type_) {
parent()->RemoveChildView(this);
}
}
private:
// The event type which causes the view to remove itself.
ui::EventType remove_event_type_;
};
BEGIN_METADATA(RemoveViewOnEvent)
END_METADATA
// View class which generates a nested event the first time it gets an event of
// type |nested_event_type|. This is used to simulate nested event loops which
// can cause |RootView::mouse_event_handler_| to get reset.
class NestedEventOnEvent : public View {
METADATA_HEADER(NestedEventOnEvent, View)
public:
NestedEventOnEvent(ui::EventType nested_event_type, View* root_view)
: nested_event_type_(nested_event_type), root_view_(root_view) {}
NestedEventOnEvent(const NestedEventOnEvent&) = delete;
NestedEventOnEvent& operator=(const NestedEventOnEvent&) = delete;
void OnEvent(ui::Event* event) override {
if (event->type() == nested_event_type_) {
ui::MouseEvent exit_event(ui::EventType::kMouseExited, gfx::Point(),
gfx::Point(), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
// Avoid infinite recursion if |nested_event_type_| ==
// EventType::kMouseExited.
nested_event_type_ = ui::EventType::kUnknown;
root_view_->OnMouseExited(exit_event);
}
}
private:
// The event type which causes the view to generate a nested event.
ui::EventType nested_event_type_;
// root view of this view; owned by widget.
raw_ptr<View> root_view_;
};
BEGIN_METADATA(NestedEventOnEvent)
END_METADATA
} // namespace
// Verifies deleting a View in OnMouseExited() doesn't crash.
TEST_F(RootViewTest, DeleteViewOnMouseExitDispatch) {
RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
.type = Widget::InitParams::TYPE_POPUP});
internal::RootView* root_view = state.GetRootView();
bool view_destroyed = false;
View* child = state.AddChildView(std::make_unique<DeleteViewOnEvent>(
ui::EventType::kMouseExited, &view_destroyed));
child->SetBounds(10, 10, 500, 500);
// Generate a mouse move event which ensures that |mouse_moved_handler_|
// is set in the RootView class.
ui::MouseEvent moved_event(ui::EventType::kMouseMoved, gfx::Point(15, 15),
gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(moved_event);
ASSERT_FALSE(view_destroyed);
// Generate a mouse exit event which in turn will delete the child view which
// was the target of the mouse move event above. This should not crash when
// the mouse exit handler returns from the child.
ui::MouseEvent exit_event(ui::EventType::kMouseExited, gfx::Point(),
gfx::Point(), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseExited(exit_event);
EXPECT_TRUE(view_destroyed);
EXPECT_TRUE(root_view->GetContentsView()->children().empty());
}
// Verifies deleting a View in OnMouseEntered() doesn't crash.
TEST_F(RootViewTest, DeleteViewOnMouseEnterDispatch) {
RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
.type = Widget::InitParams::TYPE_POPUP});
internal::RootView* root_view = state.GetRootView();
bool view_destroyed = false;
View* child = state.AddChildView(std::make_unique<DeleteViewOnEvent>(
ui::EventType::kMouseEntered, &view_destroyed));
// Make |child| smaller than the containing Widget and RootView.
child->SetBounds(100, 100, 100, 100);
// Move the mouse within |widget| but outside of |child|.
ui::MouseEvent moved_event(ui::EventType::kMouseMoved, gfx::Point(15, 15),
gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(moved_event);
ASSERT_FALSE(view_destroyed);
// Move the mouse within |child|, which should dispatch a mouse enter event to
// |child| and destroy the view. This should not crash when the mouse enter
// handler returns from the child.
ui::MouseEvent moved_event2(ui::EventType::kMouseMoved, gfx::Point(115, 115),
gfx::Point(115, 115), ui::EventTimeForNow(), 0,
0);
root_view->OnMouseMoved(moved_event2);
EXPECT_TRUE(view_destroyed);
EXPECT_TRUE(root_view->GetContentsView()->children().empty());
}
// Verifies removing a View in OnMouseEntered() doesn't crash.
TEST_F(RootViewTest, RemoveViewOnMouseEnterDispatch) {
RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
.type = Widget::InitParams::TYPE_POPUP});
internal::RootView* root_view = state.GetRootView();
View* content = root_view->GetContentsView();
// |child| gets removed without being deleted, so make it a local
// to prevent test memory leak.
RemoveViewOnEvent child(ui::EventType::kMouseEntered);
content->AddChildViewRaw(&child);
// Make |child| smaller than the containing Widget and RootView.
child.SetBounds(100, 100, 100, 100);
// Move the mouse within |widget| but outside of |child|.
ui::MouseEvent moved_event(ui::EventType::kMouseMoved, gfx::Point(15, 15),
gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(moved_event);
// Move the mouse within |child|, which should dispatch a mouse enter event to
// |child| and remove the view. This should not crash when the mouse enter
// handler returns.
ui::MouseEvent moved_event2(ui::EventType::kMouseMoved, gfx::Point(115, 115),
gfx::Point(115, 115), ui::EventTimeForNow(), 0,
0);
root_view->OnMouseMoved(moved_event2);
EXPECT_TRUE(content->children().empty());
}
// Verifies clearing the root view's |mouse_move_handler_| in OnMouseExited()
// doesn't crash.
TEST_F(RootViewTest, ClearMouseMoveHandlerOnMouseExitDispatch) {
RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
.type = Widget::InitParams::TYPE_POPUP});
internal::RootView* root_view = state.GetRootView();
View* child = state.AddChildView(std::make_unique<NestedEventOnEvent>(
ui::EventType::kMouseExited, root_view));
// Make |child| smaller than the containing Widget and RootView.
child->SetBounds(100, 100, 100, 100);
// Generate a mouse move event which ensures that |mouse_moved_handler_|
// is set to the child view in the RootView class.
ui::MouseEvent moved_event(ui::EventType::kMouseMoved, gfx::Point(110, 110),
gfx::Point(110, 110), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(moved_event);
// Move the mouse outside of |child| which causes a mouse exit event to be
// dispatched to |child|, which will in turn generate a nested event that
// clears |mouse_move_handler_|. This should not crash
// RootView::OnMouseMoved.
ui::MouseEvent move_event2(ui::EventType::kMouseMoved, gfx::Point(15, 15),
gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(move_event2);
}
// Verifies clearing the root view's |mouse_move_handler_| in OnMouseExited()
// doesn't crash, in the case where the root view is targeted, because
// it's the first enabled view encountered walking up the target tree.
TEST_F(RootViewTest,
ClearMouseMoveHandlerOnMouseExitDispatchWithContentViewDisabled) {
RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
.type = Widget::InitParams::TYPE_POPUP});
internal::RootView* root_view = state.GetRootView();
View* child = state.AddChildView(std::make_unique<NestedEventOnEvent>(
ui::EventType::kMouseExited, root_view));
// Make |child| smaller than the containing Widget and RootView.
child->SetBounds(100, 100, 100, 100);
// Generate a mouse move event which ensures that the |mouse_moved_handler_|
// member is set to the child view in the RootView class.
ui::MouseEvent moved_event(ui::EventType::kMouseMoved, gfx::Point(110, 110),
gfx::Point(110, 110), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(moved_event);
// This will make RootView::OnMouseMoved skip the content view when looking
// for a handler for the mouse event, and instead use the root view.
root_view->GetContentsView()->SetEnabled(false);
// Move the mouse outside of |child| which should dispatch a mouse exit event
// to |mouse_move_handler_| (currently |child|), which will in turn generate a
// nested event that clears |mouse_move_handler_|. This should not crash
// RootView::OnMouseMoved.
ui::MouseEvent move_event2(ui::EventType::kMouseMoved, gfx::Point(200, 200),
gfx::Point(200, 200), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(move_event2);
}
// Verifies clearing the root view's |mouse_move_handler_| in OnMouseEntered()
// doesn't crash.
TEST_F(RootViewTest, ClearMouseMoveHandlerOnMouseEnterDispatch) {
RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
.type = Widget::InitParams::TYPE_POPUP});
internal::RootView* root_view = state.GetRootView();
View* child = state.AddChildView(std::make_unique<NestedEventOnEvent>(
ui::EventType::kMouseEntered, root_view));
// Make |child| smaller than the containing Widget and RootView.
child->SetBounds(100, 100, 100, 100);
// Move the mouse within |widget| but outside of |child|.
ui::MouseEvent moved_event(ui::EventType::kMouseMoved, gfx::Point(15, 15),
gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(moved_event);
// Move the mouse within |child|, which dispatches a mouse enter event to
// |child| and resets the root view's |mouse_move_handler_|. This should not
// crash when the mouse enter handler generates an EventType::kMouseEntered
// event.
ui::MouseEvent moved_event2(ui::EventType::kMouseMoved, gfx::Point(115, 115),
gfx::Point(115, 115), ui::EventTimeForNow(), 0,
0);
root_view->OnMouseMoved(moved_event2);
}
namespace {
// View class which deletes its owning Widget when it gets a mouse exit event.
class DeleteWidgetOnMouseExit : public View {
METADATA_HEADER(DeleteWidgetOnMouseExit, View)
public:
explicit DeleteWidgetOnMouseExit(base::OnceClosure on_mouse_exit_callback)
: on_mouse_exit_callback_(std::move(on_mouse_exit_callback)) {}
DeleteWidgetOnMouseExit(const DeleteWidgetOnMouseExit&) = delete;
DeleteWidgetOnMouseExit& operator=(const DeleteWidgetOnMouseExit&) = delete;
~DeleteWidgetOnMouseExit() override = default;
void OnMouseExited(const ui::MouseEvent& event) override {
std::move(on_mouse_exit_callback_).Run();
}
private:
base::OnceClosure on_mouse_exit_callback_;
};
BEGIN_METADATA(DeleteWidgetOnMouseExit)
END_METADATA
} // namespace
// Test that there is no crash if a View deletes its parent Widget in
// View::OnMouseExited().
TEST_F(RootViewTest, DeleteWidgetOnMouseExitDispatch) {
auto widget = std::make_unique<Widget>();
Widget::InitParams init_params = CreateParams(
Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
widget->Init(std::move(init_params));
widget->SetBounds(gfx::Rect(10, 10, 500, 500));
WidgetDeletionObserver widget_deletion_observer(widget.get());
auto content = std::make_unique<View>();
auto* child = content->AddChildView(std::make_unique<DeleteWidgetOnMouseExit>(
base::BindOnce([](std::unique_ptr<Widget>* widget) { widget->reset(); },
base::Unretained(&widget))));
widget->SetContentsView(std::move(content));
// Make |child| smaller than the containing Widget and RootView.
child->SetBounds(100, 100, 100, 100);
internal::RootView* root_view =
static_cast<internal::RootView*>(widget->GetRootView());
// Move the mouse within |child|.
ui::MouseEvent moved_event(ui::EventType::kMouseMoved, gfx::Point(115, 115),
gfx::Point(115, 115), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(moved_event);
ASSERT_TRUE(widget_deletion_observer.IsWidgetAlive());
// Move the mouse outside of |child| which should dispatch a mouse exit event
// to |child| and destroy the widget. This should not crash when the mouse
// exit handler returns from the child.
ui::MouseEvent move_event2(ui::EventType::kMouseMoved, gfx::Point(15, 15),
gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(move_event2);
EXPECT_FALSE(widget_deletion_observer.IsWidgetAlive());
}
// Test that there is no crash if a View deletes its parent widget as a result
// of a mouse exited event which was propagated from one of its children.
TEST_F(RootViewTest, DeleteWidgetOnMouseExitDispatchFromChild) {
auto widget = std::make_unique<Widget>();
Widget::InitParams init_params = CreateParams(
Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
widget->Init(std::move(init_params));
widget->SetBounds(gfx::Rect(10, 10, 500, 500));
WidgetDeletionObserver widget_deletion_observer(widget.get());
auto* content = widget->SetContentsView(std::make_unique<View>());
auto* child = content->AddChildView(std::make_unique<DeleteWidgetOnMouseExit>(
base::BindOnce([](std::unique_ptr<Widget>* widget) { widget->reset(); },
base::Unretained(&widget))));
auto* subchild = child->AddChildView(std::make_unique<View>());
// Make |child| and |subchild| smaller than the containing Widget and
// RootView.
child->SetBounds(100, 100, 100, 100);
subchild->SetBounds(0, 0, 100, 100);
// Make mouse enter and exit events get propagated from |subchild| to |child|.
child->SetNotifyEnterExitOnChild(true);
internal::RootView* root_view =
static_cast<internal::RootView*>(widget->GetRootView());
// Move the mouse within |subchild| and |child|.
ui::MouseEvent moved_event(ui::EventType::kMouseMoved, gfx::Point(115, 115),
gfx::Point(115, 115), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(moved_event);
ASSERT_TRUE(widget_deletion_observer.IsWidgetAlive());
// Move the mouse outside of |subchild| and |child| which should dispatch a
// mouse exit event to |subchild| and destroy the widget. This should not
// crash when the mouse exit handler returns from |subchild|.
ui::MouseEvent move_event2(ui::EventType::kMouseMoved, gfx::Point(15, 15),
gfx::Point(15, 15), ui::EventTimeForNow(), 0, 0);
root_view->OnMouseMoved(move_event2);
EXPECT_FALSE(widget_deletion_observer.IsWidgetAlive());
}
class RootViewTestDialogDelegate : public DialogDelegateView {
public:
RootViewTestDialogDelegate() {
// Ensure that buttons don't influence the layout.
DialogDelegate::SetButtons(
static_cast<int>(ui::mojom::DialogButton::kNone));
}
RootViewTestDialogDelegate(const RootViewTestDialogDelegate&) = delete;
RootViewTestDialogDelegate& operator=(const RootViewTestDialogDelegate&) =
delete;
~RootViewTestDialogDelegate() override = default;
int layout_count() const { return layout_count_; }
// DialogDelegateView:
gfx::Size CalculatePreferredSize(
const SizeBounds& /*available_size*/) const override {
return preferred_size_;
}
void Layout(PassKey) override {
EXPECT_EQ(size(), preferred_size_);
++layout_count_;
}
private:
const gfx::Size preferred_size_ = gfx::Size(111, 111);
int layout_count_ = 0;
};
// Ensure only one layout happens during Widget initialization, and ensure it
// happens at the ContentView's preferred size.
TEST_F(RootViewTest, SingleLayoutDuringInit) {
RootViewTestDialogDelegate* delegate = new RootViewTestDialogDelegate();
Widget* widget = DialogDelegate::CreateDialogWidget(delegate, GetContext(),
gfx::NativeView());
EXPECT_EQ(1, delegate->layout_count());
widget->CloseNow();
}
using RootViewDesktopNativeWidgetTest = ViewsTestWithDesktopNativeWidget;
// Also test Aura desktop Widget codepaths.
TEST_F(RootViewDesktopNativeWidgetTest, SingleLayoutDuringInit) {
RootViewTestDialogDelegate* delegate = new RootViewTestDialogDelegate();
Widget* widget = DialogDelegate::CreateDialogWidget(delegate, GetContext(),
gfx::NativeView());
EXPECT_EQ(1, delegate->layout_count());
widget->CloseNow();
}
TEST_F(RootViewTest, UpdateAccessibleURL) {
RootViewTestState state(this, {.bounds = {100, 100, 100, 100}});
internal::RootView* root_view = state.GetRootView();
const GURL test_url("https://example.com");
root_view->UpdateAccessibleURL(test_url);
ui::AXNodeData node_data;
root_view->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_EQ(node_data.GetStringAttribute(ax::mojom::StringAttribute::kUrl),
test_url);
}
#if !BUILDFLAG(IS_MAC)
// Tests that AnnounceAlert sets up the correct text value on the hidden
// view, and that the resulting hidden view actually stays hidden.
TEST_F(RootViewTest, AnnounceTextAsTest) {
RootViewTestState state(this, {.bounds = {100, 100, 100, 100}});
internal::RootView* root_view = state.GetRootView();
EXPECT_EQ(1U, root_view->children().size());
const std::u16string kAlertText = u"Alert";
root_view->AnnounceTextAs(kAlertText,
ui::AXPlatformNode::AnnouncementType::kAlert);
EXPECT_EQ(2U, root_view->children().size());
views::test::RunScheduledLayout(root_view);
EXPECT_FALSE(root_view->children()[0]->size().IsEmpty());
EXPECT_TRUE(root_view->children()[1]->size().IsEmpty());
View* const hidden_alert_view = root_view->children()[1];
ui::AXNodeData node_data;
hidden_alert_view->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_EQ(kAlertText,
node_data.GetString16Attribute(ax::mojom::StringAttribute::kName));
#if BUILDFLAG(IS_CHROMEOS)
EXPECT_EQ(node_data.role, ax::mojom::Role::kStaticText);
#elif BUILDFLAG(IS_LINUX)
EXPECT_EQ(node_data.role, ax::mojom::Role::kAlert);
#else
EXPECT_EQ(node_data.role, ax::mojom::Role::kAlert);
#endif
EXPECT_TRUE(node_data.HasState(ax::mojom::State::kInvisible));
const std::u16string kPoliteText = u"Something polite";
root_view->AnnounceTextAs(kPoliteText,
ui::AXPlatformNode::AnnouncementType::kPolite);
View* const hidden_polite_view = root_view->children()[1];
hidden_polite_view->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_EQ(kPoliteText,
node_data.GetString16Attribute(ax::mojom::StringAttribute::kName));
hidden_polite_view->GetViewAccessibility().GetAccessibleNodeData(&node_data);
EXPECT_TRUE(node_data.HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus));
const std::string& val = node_data.GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus);
EXPECT_EQ("polite", val);
#if BUILDFLAG(IS_CHROMEOS)
EXPECT_EQ(node_data.role, ax::mojom::Role::kStaticText);
#elif BUILDFLAG(IS_LINUX)
EXPECT_EQ(node_data.role, ax::mojom::Role::kAlert);
#else
EXPECT_EQ(node_data.role, ax::mojom::Role::kStatus);
#endif
EXPECT_TRUE(
node_data.GetBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic));
EXPECT_EQ("polite", node_data.GetStringAttribute(
ax::mojom::StringAttribute::kLiveStatus));
EXPECT_EQ("additions text", node_data.GetStringAttribute(
ax::mojom::StringAttribute::kLiveRelevant));
EXPECT_EQ("additions text",
node_data.GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveRelevant));
}
#endif // !BUILDFLAG(IS_MAC)
TEST_F(RootViewTest, MouseEventDispatchedToClosestEnabledView) {
RootViewTestState state(this, {.bounds = {100, 100, 100, 100}});
internal::RootView* root_view = state.GetRootView();
View* const contents_view = root_view->GetContentsView();
EventCountView* const v1 =
contents_view->AddChildView(std::make_unique<EventCountView>());
EventCountView* const v2 =
v1->AddChildView(std::make_unique<EventCountView>());
EventCountView* const v3 =
v2->AddChildView(std::make_unique<EventCountView>());
contents_view->SetBoundsRect(gfx::Rect(0, 0, 10, 10));
v1->SetBoundsRect(gfx::Rect(0, 0, 10, 10));
v2->SetBoundsRect(gfx::Rect(0, 0, 10, 10));
v3->SetBoundsRect(gfx::Rect(0, 0, 10, 10));
v1->set_handle_mode(EventCountView::HandleMode::kConsumeEvents);
v2->set_handle_mode(EventCountView::HandleMode::kConsumeEvents);
v3->set_handle_mode(EventCountView::HandleMode::kConsumeEvents);
ui::MouseEvent pressed_event(ui::EventType::kMousePressed, gfx::Point(5, 5),
gfx::Point(5, 5), ui::EventTimeForNow(), 0, 0);
ui::MouseEvent released_event(ui::EventType::kMouseReleased, gfx::Point(5, 5),
gfx::Point(5, 5), ui::EventTimeForNow(), 0, 0);
root_view->OnMousePressed(pressed_event);
root_view->OnMouseReleased(released_event);
EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kMousePressed));
EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMousePressed));
EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kMousePressed));
v3->SetEnabled(false);
root_view->OnMousePressed(pressed_event);
root_view->OnMouseReleased(released_event);
EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kMousePressed));
EXPECT_EQ(1, v2->GetEventCount(ui::EventType::kMousePressed));
EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kMousePressed));
v3->SetEnabled(true);
v2->SetEnabled(false);
root_view->OnMousePressed(pressed_event);
root_view->OnMouseReleased(released_event);
EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kMousePressed));
EXPECT_EQ(1, v2->GetEventCount(ui::EventType::kMousePressed));
EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kMousePressed));
}
// If RootView::OnMousePressed() receives a double-click event that isn't
// handled by any views, it should still report it as handled if the first click
// was handled. However, it should *not* if the first click was unhandled.
// Regression test for https://crbug.com/1055674.
TEST_F(RootViewTest, DoubleClickHandledIffFirstClickHandled) {
RootViewTestState state(this, {.bounds = {100, 100, 100, 100}});
internal::RootView* root_view = state.GetRootView();
View* const contents_view = root_view->GetContentsView();
EventCountView* const v1 =
contents_view->AddChildView(std::make_unique<EventCountView>());
contents_view->SetBoundsRect(gfx::Rect(0, 0, 10, 10));
v1->SetBoundsRect(gfx::Rect(0, 0, 10, 10));
ui::MouseEvent pressed_event(ui::EventType::kMousePressed, gfx::Point(5, 5),
gfx::Point(5, 5), ui::EventTimeForNow(), 0, 0);
ui::MouseEvent released_event(ui::EventType::kMouseReleased, gfx::Point(5, 5),
gfx::Point(5, 5), ui::EventTimeForNow(), 0, 0);
// First click handled, second click unhandled.
v1->set_handle_mode(EventCountView::HandleMode::kConsumeEvents);
pressed_event.SetClickCount(1);
released_event.SetClickCount(1);
EXPECT_TRUE(root_view->OnMousePressed(pressed_event));
root_view->OnMouseReleased(released_event);
v1->set_handle_mode(EventCountView::HandleMode::kPropagateEvents);
pressed_event.SetClickCount(2);
released_event.SetClickCount(2);
EXPECT_TRUE(root_view->OnMousePressed(pressed_event));
root_view->OnMouseReleased(released_event);
// Both clicks unhandled.
v1->set_handle_mode(EventCountView::HandleMode::kPropagateEvents);
pressed_event.SetClickCount(1);
released_event.SetClickCount(1);
EXPECT_FALSE(root_view->OnMousePressed(pressed_event));
root_view->OnMouseReleased(released_event);
pressed_event.SetClickCount(2);
released_event.SetClickCount(2);
EXPECT_FALSE(root_view->OnMousePressed(pressed_event));
root_view->OnMouseReleased(released_event);
}
TEST_F(RootViewTest, AccessibleProperties) {
RootViewTestState state(this);
internal::RootView* root_view = state.GetRootView();
ui::AXNodeData data;
root_view->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.role, ax::mojom::Role::kWindow);
}
TEST_F(RootViewTest, AccessibleName) {
RootViewTestState state(this);
internal::RootView* root_view = state.GetRootView();
ui::AXNodeData data;
root_view->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
state.widget()->widget_delegate()->GetAccessibleWindowTitle());
state.widget()->widget_delegate()->SetTitle(u"Sample Title");
data = ui::AXNodeData();
root_view->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
state.widget()->widget_delegate()->GetAccessibleWindowTitle());
state.widget()->widget_delegate()->SetAccessibleTitle(
u"Sample Accessible Title");
data = ui::AXNodeData();
root_view->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
state.widget()->widget_delegate()->GetAccessibleWindowTitle());
}
TEST_F(RootViewTest, AccessibleNameChangeEvent) {
RootViewTestState state(this);
internal::RootView* root_view = state.GetRootView();
// TODO (crbug.com/380927771). Once VoiceOver has incorporated the name
// change event, remove all Mac specific code.
#if BUILDFLAG(IS_MAC)
base::test::ScopedFeatureList feature_list{
::features::kBlockRootWindowAccessibleNameChangeEvent};
#endif
views::test::AXEventCounter counter(views::AXUpdateNotifier::Get());
state.widget()->widget_delegate()->SetTitle(u"Sample Title");
#if BUILDFLAG(IS_MAC)
EXPECT_TRUE(::features::IsBlockRootWindowAccessibleNameChangeEventEnabled());
EXPECT_EQ(0, counter.GetCount(ax::mojom::Event::kTextChanged, root_view));
#endif
#if !BUILDFLAG(IS_MAC)
EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kTextChanged, root_view));
#endif
#if BUILDFLAG(IS_MAC)
base::test::ScopedFeatureList disable_feature_list;
disable_feature_list.InitWithFeatures(
{}, {::features::kBlockRootWindowAccessibleNameChangeEvent});
#endif
state.widget()->widget_delegate()->SetAccessibleTitle(
u"Sample Accessible Title");
#if BUILDFLAG(IS_MAC)
EXPECT_FALSE(::features::IsBlockRootWindowAccessibleNameChangeEventEnabled());
EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kTextChanged, root_view));
#endif
#if !BUILDFLAG(IS_MAC)
EXPECT_EQ(2, counter.GetCount(ax::mojom::Event::kTextChanged, root_view));
#endif
}
} // namespace views::test