| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <algorithm> |
| #include <memory> |
| #include <optional> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/accessibility/accessibility_features.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/cursor/cursor.h" |
| #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" |
| #include "ui/base/hit_test.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/mojom/ui_base_types.mojom-shared.h" |
| #include "ui/base/mojom/window_show_state.mojom.h" |
| #include "ui/color/color_id.h" |
| #include "ui/color/color_provider.h" |
| #include "ui/color/color_provider_key.h" |
| #include "ui/color/color_provider_manager.h" |
| #include "ui/color/color_recipe.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_animation_observer.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/compositor/test/draw_waiter_for_test.h" |
| #include "ui/events/event_observer.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/test/event_generator.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/native_widget_types.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/native_theme/test_native_theme.h" |
| #include "ui/views/bubble/bubble_dialog_delegate_view.h" |
| #include "ui/views/buildflags.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/event_monitor.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/style/platform_style.h" |
| #include "ui/views/test/configurable_test_frame_view.h" |
| #include "ui/views/test/mock_drag_controller.h" |
| #include "ui/views/test/mock_native_widget.h" |
| #include "ui/views/test/native_widget_factory.h" |
| #include "ui/views/test/test_views.h" |
| #include "ui/views/test/test_widget_observer.h" |
| #include "ui/views/test/views_test_utils.h" |
| #include "ui/views/test/widget_test.h" |
| #include "ui/views/view_test_api.h" |
| #include "ui/views/views_test_suite.h" |
| #include "ui/views/widget/native_widget_delegate.h" |
| #include "ui/views/widget/native_widget_private.h" |
| #include "ui/views/widget/root_view.h" |
| #include "ui/views/widget/unique_widget_ptr.h" |
| #include "ui/views/widget/widget_delegate.h" |
| #include "ui/views/widget/widget_deletion_observer.h" |
| #include "ui/views/widget/widget_interactive_uitest_utils.h" |
| #include "ui/views/widget/widget_removals_observer.h" |
| #include "ui/views/widget/widget_utils.h" |
| #include "ui/views/window/dialog_delegate.h" |
| #include "ui/views/window/native_frame_view.h" |
| |
| #if defined(USE_AURA) |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/view_prop.h" |
| #include "ui/views/test/test_platform_native_widget.h" |
| #include "ui/views/widget/native_widget_aura.h" |
| #include "ui/wm/core/base_focus_rules.h" |
| #include "ui/wm/core/focus_controller.h" |
| #include "ui/wm/core/shadow_controller.h" |
| #include "ui/wm/core/shadow_controller_delegate.h" |
| #endif |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "ui/base/win/window_event_target.h" |
| #include "ui/views/win/hwnd_util.h" |
| #endif |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "base/mac/mac_util.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" |
| #endif |
| |
| namespace views::test { |
| |
| namespace { |
| |
| using ::testing::_; |
| using ::testing::IsEmpty; |
| using ::testing::Not; |
| |
| // TODO(tdanderson): This utility function is used in different unittest |
| // files. Move to a common location to avoid |
| // repeated code. |
| gfx::Point ConvertPointFromWidgetToView(View* view, const gfx::Point& p) { |
| gfx::Point tmp(p); |
| View::ConvertPointToTarget(view->GetWidget()->GetRootView(), view, &tmp); |
| return tmp; |
| } |
| |
| std::unique_ptr<ui::test::EventGenerator> CreateEventGenerator( |
| gfx::NativeWindow root_window, |
| gfx::NativeWindow target_window) { |
| auto generator = |
| std::make_unique<ui::test::EventGenerator>(root_window, target_window); |
| return generator; |
| } |
| |
| // Convenience to make constructing a GestureEvent simpler. |
| ui::GestureEvent CreateTestGestureEvent(ui::EventType type, int x, int y) { |
| return ui::GestureEvent(x, y, 0, base::TimeTicks(), |
| ui::GestureEventDetails(type)); |
| } |
| |
| ui::GestureEvent CreateTestGestureEvent(const ui::GestureEventDetails& details, |
| int x, |
| int y) { |
| return ui::GestureEvent(x, y, 0, base::TimeTicks(), details); |
| } |
| |
| std::unique_ptr<NativeFrameView> CreateMinimumSizeFrameView(Widget* frame) { |
| auto frame_view = std::make_unique<ConfigurableTestFrameView>(frame); |
| frame_view->set_minimum_size(gfx::Size(300, 400)); |
| return std::move(frame_view); |
| } |
| |
| class TestWidgetRemovalsObserver : public WidgetRemovalsObserver { |
| public: |
| TestWidgetRemovalsObserver() = default; |
| |
| TestWidgetRemovalsObserver(const TestWidgetRemovalsObserver&) = delete; |
| TestWidgetRemovalsObserver& operator=(const TestWidgetRemovalsObserver&) = |
| delete; |
| |
| ~TestWidgetRemovalsObserver() override = default; |
| |
| void OnWillRemoveView(Widget* widget, View* view) override { |
| removed_views_.insert(view); |
| } |
| |
| bool DidRemoveView(View* view) { |
| return removed_views_.find(view) != removed_views_.end(); |
| } |
| |
| private: |
| std::set<raw_ptr<View, SetExperimental>> removed_views_; |
| }; |
| |
| } // namespace |
| |
| class WidgetTestBubbleDialogDelegateView : public BubbleDialogDelegateView { |
| METADATA_HEADER(WidgetTestBubbleDialogDelegateView, BubbleDialogDelegateView) |
| |
| public: |
| explicit WidgetTestBubbleDialogDelegateView(View* anchor) |
| : BubbleDialogDelegateView(anchor, BubbleBorder::NONE) {} |
| ~WidgetTestBubbleDialogDelegateView() override = default; |
| |
| bool ShouldShowCloseButton() const override { |
| reset_controls_called_ = true; |
| return true; |
| } |
| |
| mutable bool reset_controls_called_ = false; |
| }; |
| |
| BEGIN_METADATA(WidgetTestBubbleDialogDelegateView) |
| END_METADATA |
| |
| // A view that keeps track of the events it receives, and consumes all scroll |
| // gesture events and ui::EventType::kScroll events. |
| class ScrollableEventCountView : public EventCountView { |
| METADATA_HEADER(ScrollableEventCountView, EventCountView) |
| |
| public: |
| ScrollableEventCountView() = default; |
| |
| ScrollableEventCountView(const ScrollableEventCountView&) = delete; |
| ScrollableEventCountView& operator=(const ScrollableEventCountView&) = delete; |
| |
| ~ScrollableEventCountView() override = default; |
| |
| private: |
| // Overridden from ui::EventHandler: |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| EventCountView::OnGestureEvent(event); |
| switch (event->type()) { |
| case ui::EventType::kGestureScrollBegin: |
| case ui::EventType::kGestureScrollUpdate: |
| case ui::EventType::kGestureScrollEnd: |
| case ui::EventType::kScrollFlingStart: |
| event->SetHandled(); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| void OnScrollEvent(ui::ScrollEvent* event) override { |
| EventCountView::OnScrollEvent(event); |
| if (event->type() == ui::EventType::kScroll) { |
| event->SetHandled(); |
| } |
| } |
| }; |
| |
| BEGIN_METADATA(ScrollableEventCountView) |
| END_METADATA |
| |
| // An event handler that simply keeps a count of the different types of events |
| // it receives. |
| class EventCountHandler : public ui::EventHandler { |
| public: |
| EventCountHandler() = default; |
| |
| EventCountHandler(const EventCountHandler&) = delete; |
| EventCountHandler& operator=(const EventCountHandler&) = delete; |
| |
| ~EventCountHandler() override = default; |
| |
| int GetEventCount(ui::EventType type) { return event_count_[type]; } |
| |
| void ResetCounts() { event_count_.clear(); } |
| |
| protected: |
| // Overridden from ui::EventHandler: |
| void OnEvent(ui::Event* event) override { |
| RecordEvent(*event); |
| ui::EventHandler::OnEvent(event); |
| } |
| |
| private: |
| void RecordEvent(const ui::Event& event) { ++event_count_[event.type()]; } |
| |
| std::map<ui::EventType, int> event_count_; |
| }; |
| |
| TEST_F(WidgetTest, WidgetInitParams) { |
| // Widgets are not transparent by default. |
| Widget::InitParams init1(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| EXPECT_EQ(Widget::InitParams::WindowOpacity::kInferred, init1.opacity); |
| } |
| |
| // Tests that the internal name is propagated through widget initialization to |
| // the native widget and back. |
| class WidgetWithCustomParamsTest : public WidgetTest { |
| public: |
| using InitFunction = base::RepeatingCallback<void(Widget::InitParams*)>; |
| void SetInitFunction(const InitFunction& init) { init_ = std::move(init); } |
| Widget::InitParams CreateParams(Widget::InitParams::Ownership ownership, |
| Widget::InitParams::Type type) override { |
| Widget::InitParams params = WidgetTest::CreateParams(ownership, type); |
| DCHECK(init_) << "If you don't need an init function, use WidgetTest"; |
| init_.Run(¶ms); |
| return params; |
| } |
| |
| private: |
| InitFunction init_; |
| }; |
| |
| TEST_F(WidgetWithCustomParamsTest, NamePropagatedFromParams) { |
| SetInitFunction(base::BindLambdaForTesting( |
| [](Widget::InitParams* params) { params->name = "MyWidget"; })); |
| std::unique_ptr<Widget> widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| |
| EXPECT_EQ("MyWidget", widget->native_widget_private()->GetName()); |
| EXPECT_EQ("MyWidget", widget->GetName()); |
| } |
| |
| TEST_F(WidgetWithCustomParamsTest, NamePropagatedFromDelegate) { |
| WidgetDelegate delegate; |
| delegate.set_internal_name("Foobar"); |
| SetInitFunction(base::BindLambdaForTesting( |
| [&](Widget::InitParams* params) { params->delegate = &delegate; })); |
| |
| std::unique_ptr<Widget> widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| |
| EXPECT_EQ(delegate.internal_name(), |
| widget->native_widget_private()->GetName()); |
| EXPECT_EQ(delegate.internal_name(), widget->GetName()); |
| } |
| |
| // Test that Widget::InitParams::autosize allows widget to |
| // automatically resize when content view size changes. |
| TEST_F(WidgetWithCustomParamsTest, Autosize) { |
| SetInitFunction(base::BindLambdaForTesting( |
| [](Widget::InitParams* params) { params->autosize = true; })); |
| |
| std::unique_ptr<Widget> widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| auto* view = widget->SetContentsView(std::make_unique<views::View>()); |
| widget->Show(); |
| |
| constexpr gfx::Size kInitialSize(100, 100); |
| view->SetPreferredSize(kInitialSize); |
| |
| // RunScheduledLayout() is needed due to widget auto-resize. |
| views::test::RunScheduledLayout(widget.get()); |
| const gfx::Size starting_size = widget->GetWindowBoundsInScreen().size(); |
| |
| constexpr gfx::Size kDelta(50, 50); |
| view->SetPreferredSize(kInitialSize + kDelta); |
| |
| // RunScheduledLayout() is needed due to widget auto-resize. |
| views::test::RunScheduledLayout(widget.get()); |
| const gfx::Size ending_size = widget->GetWindowBoundsInScreen().size(); |
| |
| EXPECT_EQ(ending_size, starting_size + kDelta); |
| } |
| |
| namespace { |
| |
| class ViewWithClassName : public View { |
| METADATA_HEADER(ViewWithClassName, View) |
| }; |
| |
| BEGIN_METADATA(ViewWithClassName) |
| END_METADATA |
| |
| } // namespace |
| |
| TEST_F(WidgetWithCustomParamsTest, NamePropagatedFromContentsViewClassName) { |
| WidgetDelegate delegate; |
| auto view = std::make_unique<ViewWithClassName>(); |
| auto* contents = delegate.SetContentsView(std::move(view)); |
| SetInitFunction(base::BindLambdaForTesting( |
| [&](Widget::InitParams* params) { params->delegate = &delegate; })); |
| |
| std::unique_ptr<Widget> widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| |
| EXPECT_EQ(contents->GetClassName(), |
| widget->native_widget_private()->GetName()); |
| EXPECT_EQ(contents->GetClassName(), widget->GetName()); |
| } |
| |
| namespace { |
| |
| class TestView : public View { |
| METADATA_HEADER(TestView, View) |
| |
| public: |
| ~TestView() override = default; |
| |
| void OnThemeChanged() override { |
| View::OnThemeChanged(); |
| auto* native_theme = GetNativeTheme(); |
| if (native_theme && native_theme->user_color()) { |
| user_color_ = *native_theme->user_color(); |
| } |
| } |
| |
| SkColor user_color() const { return user_color_; } |
| |
| private: |
| SkColor user_color_ = SK_ColorWHITE; |
| }; |
| |
| BEGIN_METADATA(TestView) |
| END_METADATA |
| |
| } // namespace |
| |
| class WidgetColorModeTest : public WidgetTest { |
| public: |
| static constexpr SkColor kLightColor = SK_ColorWHITE; |
| static constexpr SkColor kDarkColor = SK_ColorBLACK; |
| |
| WidgetColorModeTest() = default; |
| ~WidgetColorModeTest() override = default; |
| |
| void SetUp() override { |
| WidgetTest::SetUp(); |
| |
| // Setup color provider for the ui::kColorSysPrimary color. |
| ui::ColorProviderManager& manager = |
| ui::ColorProviderManager::GetForTesting(); |
| manager.AppendColorProviderInitializer(base::BindRepeating(&AddColor)); |
| } |
| |
| void TearDown() override { |
| ui::ColorProviderManager::ResetForTesting(); |
| WidgetTest::TearDown(); |
| } |
| |
| private: |
| static void AddColor(ui::ColorProvider* provider, |
| const ui::ColorProviderKey& key) { |
| ui::ColorMixer& mixer = provider->AddMixer(); |
| mixer[ui::kColorSysPrimary] = { |
| key.color_mode == ui::ColorProviderKey::ColorMode::kDark ? kDarkColor |
| : kLightColor}; |
| } |
| }; |
| |
| TEST_F(WidgetColorModeTest, ColorModeOverride_NoOverride) { |
| ui::TestNativeTheme test_theme; |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| test_theme.SetDarkMode(true); |
| widget->SetNativeThemeForTest(&test_theme); |
| |
| widget->SetColorModeOverride(std::nullopt, std::nullopt); |
| // Verify that we resolve the dark color when we don't override color mode. |
| EXPECT_EQ(kDarkColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| } |
| |
| TEST_F(WidgetColorModeTest, ColorModeOverride_DarkOverride) { |
| ui::TestNativeTheme test_theme; |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| test_theme.SetDarkMode(false); |
| widget->SetNativeThemeForTest(&test_theme); |
| |
| widget->SetColorModeOverride(ui::ColorProviderKey::ColorMode::kDark, |
| std::nullopt); |
| // Verify that we resolve the light color even though the theme is dark. |
| EXPECT_EQ(kDarkColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| } |
| |
| TEST_F(WidgetColorModeTest, ColorModeOverride_LightOverride) { |
| ui::TestNativeTheme test_theme; |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| test_theme.SetDarkMode(true); |
| widget->SetNativeThemeForTest(&test_theme); |
| |
| widget->SetColorModeOverride(ui::ColorProviderKey::ColorMode::kLight, |
| std::nullopt); |
| // Verify that we resolve the light color even though the theme is dark. |
| EXPECT_EQ(kLightColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| } |
| |
| TEST_F(WidgetColorModeTest, ChildInheritsColorMode_NoOverrides) { |
| // Create the parent widget and set the native theme to dark. |
| ui::TestNativeTheme test_theme; |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| test_theme.SetDarkMode(true); |
| widget->SetNativeThemeForTest(&test_theme); |
| |
| // Create the child widget. |
| std::unique_ptr<Widget> widget_child = |
| base::WrapUnique(CreateChildPlatformWidget( |
| widget->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| // Ensure neither has an override set. The child should inherit the color mode |
| // of the parent. |
| widget->SetColorModeOverride(std::nullopt, std::nullopt); |
| widget_child->SetColorModeOverride(std::nullopt, std::nullopt); |
| EXPECT_EQ(kDarkColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| EXPECT_EQ(kDarkColor, |
| widget_child->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| |
| // Set the parent's native theme to light. The child should inherit the color |
| // mode of the parent. |
| test_theme.SetDarkMode(false); |
| EXPECT_EQ(kLightColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| EXPECT_EQ(kLightColor, |
| widget_child->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| } |
| |
| TEST_F(WidgetColorModeTest, ChildInheritsColorMode_Overrides) { |
| // Create the parent widget and set the native theme to dark. |
| ui::TestNativeTheme test_theme; |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| test_theme.SetDarkMode(true); |
| widget->SetNativeThemeForTest(&test_theme); |
| |
| // Create the child widget. |
| std::unique_ptr<Widget> widget_child = |
| base::WrapUnique(CreateChildPlatformWidget( |
| widget->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| // Ensure neither has an override set. The child should inherit the color mode |
| // of the parent. |
| widget->SetColorModeOverride(std::nullopt, std::nullopt); |
| widget_child->SetColorModeOverride(std::nullopt, std::nullopt); |
| EXPECT_EQ(kDarkColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| EXPECT_EQ(kDarkColor, |
| widget_child->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| |
| // Set the parent's override to light, then back to dark. the child should |
| // follow the parent's overridden color mode. |
| widget->SetColorModeOverride(ui::ColorProviderKey::ColorMode::kLight, |
| std::nullopt); |
| EXPECT_EQ(kLightColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| EXPECT_EQ(kLightColor, |
| widget_child->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| |
| widget->SetColorModeOverride(ui::ColorProviderKey::ColorMode::kDark, |
| std::nullopt); |
| EXPECT_EQ(kDarkColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| EXPECT_EQ(kDarkColor, |
| widget_child->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| |
| // Override the child's color mode to light. The parent should continue to |
| // report a dark color mode. |
| widget_child->SetColorModeOverride(ui::ColorProviderKey::ColorMode::kLight, |
| std::nullopt); |
| EXPECT_EQ(kDarkColor, |
| widget->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| EXPECT_EQ(kLightColor, |
| widget_child->GetColorProvider()->GetColor(ui::kColorSysPrimary)); |
| } |
| |
| TEST_F(WidgetTest, NativeWindowProperty) { |
| const char* key = "foo"; |
| int value = 3; |
| |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| EXPECT_EQ(nullptr, widget->GetNativeWindowProperty(key)); |
| |
| widget->SetNativeWindowProperty(key, &value); |
| EXPECT_EQ(&value, widget->GetNativeWindowProperty(key)); |
| |
| widget->SetNativeWindowProperty(key, nullptr); |
| EXPECT_EQ(nullptr, widget->GetNativeWindowProperty(key)); |
| } |
| |
| TEST_F(WidgetTest, GetParent) { |
| // Create a hierarchy of native widgets. |
| std::unique_ptr<Widget> toplevel = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> child = base::WrapUnique(CreateChildPlatformWidget( |
| toplevel->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> grandchild = |
| base::WrapUnique(CreateChildPlatformWidget( |
| child->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| EXPECT_EQ(nullptr, toplevel->parent()); |
| EXPECT_EQ(child.get(), grandchild->parent()); |
| EXPECT_EQ(toplevel.get(), child->parent()); |
| |
| // children should be automatically destroyed with |toplevel|. |
| } |
| |
| // Verify that there is no change in focus if |enable_arrow_key_traversal| is |
| // false (the default). |
| TEST_F(WidgetTest, ArrowKeyFocusTraversalOffByDefault) { |
| std::unique_ptr<Widget> toplevel = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| // Establish default value. |
| DCHECK(!toplevel->widget_delegate()->enable_arrow_key_traversal()); |
| |
| View* container = toplevel->client_view(); |
| container->SetLayoutManager(std::make_unique<FillLayout>()); |
| auto* const button1 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| auto* const button2 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| toplevel->Show(); |
| button1->RequestFocus(); |
| |
| ui::KeyEvent right_arrow(ui::EventType::kKeyPressed, ui::VKEY_RIGHT, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&right_arrow); |
| EXPECT_TRUE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| |
| ui::KeyEvent left_arrow(ui::EventType::kKeyPressed, ui::VKEY_LEFT, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&left_arrow); |
| EXPECT_TRUE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| |
| ui::KeyEvent up_arrow(ui::EventType::kKeyPressed, ui::VKEY_UP, ui::EF_NONE); |
| toplevel->OnKeyEvent(&up_arrow); |
| EXPECT_TRUE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| |
| ui::KeyEvent down_arrow(ui::EventType::kKeyPressed, ui::VKEY_DOWN, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&down_arrow); |
| EXPECT_TRUE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| } |
| |
| // Verify that arrow keys can change focus if |enable_arrow_key_traversal| is |
| // set to true. |
| TEST_F(WidgetTest, ArrowKeyTraversalMovesFocusBetweenViews) { |
| std::unique_ptr<Widget> toplevel = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| toplevel->widget_delegate()->SetEnableArrowKeyTraversal(true); |
| |
| View* container = toplevel->client_view(); |
| container->SetLayoutManager(std::make_unique<FillLayout>()); |
| auto* const button1 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| auto* const button2 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| auto* const button3 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| toplevel->Show(); |
| button1->RequestFocus(); |
| |
| // Right should advance focus (similar to TAB). |
| ui::KeyEvent right_arrow(ui::EventType::kKeyPressed, ui::VKEY_RIGHT, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&right_arrow); |
| EXPECT_FALSE(button1->HasFocus()); |
| EXPECT_TRUE(button2->HasFocus()); |
| EXPECT_FALSE(button3->HasFocus()); |
| |
| // Down should also advance focus. |
| ui::KeyEvent down_arrow(ui::EventType::kKeyPressed, ui::VKEY_DOWN, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&down_arrow); |
| EXPECT_FALSE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| EXPECT_TRUE(button3->HasFocus()); |
| |
| // Left should reverse focus (similar to SHIFT+TAB). |
| ui::KeyEvent left_arrow(ui::EventType::kKeyPressed, ui::VKEY_LEFT, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&left_arrow); |
| EXPECT_FALSE(button1->HasFocus()); |
| EXPECT_TRUE(button2->HasFocus()); |
| EXPECT_FALSE(button3->HasFocus()); |
| |
| // Up should also reverse focus. |
| ui::KeyEvent up_arrow(ui::EventType::kKeyPressed, ui::VKEY_UP, ui::EF_NONE); |
| toplevel->OnKeyEvent(&up_arrow); |
| EXPECT_TRUE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| EXPECT_FALSE(button3->HasFocus()); |
| |
| // Test backwards wrap-around. |
| ui::KeyEvent up_arrow2(ui::EventType::kKeyPressed, ui::VKEY_UP, ui::EF_NONE); |
| toplevel->OnKeyEvent(&up_arrow2); |
| EXPECT_FALSE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| EXPECT_TRUE(button3->HasFocus()); |
| |
| // Test forward wrap-around. |
| ui::KeyEvent down_arrow2(ui::EventType::kKeyPressed, ui::VKEY_DOWN, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&down_arrow2); |
| EXPECT_TRUE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| EXPECT_FALSE(button3->HasFocus()); |
| } |
| |
| TEST_F(WidgetTest, ArrowKeyTraversalNotInheritedByChildWidgets) { |
| std::unique_ptr<Widget> parent = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> child = base::WrapUnique(CreateChildPlatformWidget( |
| parent->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| parent->widget_delegate()->SetEnableArrowKeyTraversal(true); |
| |
| View* container = child->GetContentsView(); |
| DCHECK(container); |
| container->SetLayoutManager(std::make_unique<FillLayout>()); |
| auto* const button1 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| auto* const button2 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| parent->Show(); |
| child->Show(); |
| button1->RequestFocus(); |
| |
| // Arrow key should not cause focus change on child since only the parent |
| // Widget has |enable_arrow_key_traversal| set. |
| ui::KeyEvent right_arrow(ui::EventType::kKeyPressed, ui::VKEY_RIGHT, |
| ui::EF_NONE); |
| child->OnKeyEvent(&right_arrow); |
| EXPECT_TRUE(button1->HasFocus()); |
| EXPECT_FALSE(button2->HasFocus()); |
| } |
| |
| TEST_F(WidgetTest, ArrowKeyTraversalMayBeExplicitlyEnabledByChildWidgets) { |
| std::unique_ptr<Widget> parent = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> child = base::WrapUnique(CreateChildPlatformWidget( |
| parent->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| child->widget_delegate()->SetEnableArrowKeyTraversal(true); |
| |
| View* container = child->GetContentsView(); |
| container->SetLayoutManager(std::make_unique<FillLayout>()); |
| auto* const button1 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| auto* const button2 = |
| container->AddChildView(std::make_unique<LabelButton>()); |
| parent->Show(); |
| child->Show(); |
| button1->RequestFocus(); |
| |
| // Arrow key should cause focus key on child since child has flag set, even |
| // if the parent Widget does not. |
| ui::KeyEvent right_arrow(ui::EventType::kKeyPressed, ui::VKEY_RIGHT, |
| ui::EF_NONE); |
| child->OnKeyEvent(&right_arrow); |
| EXPECT_FALSE(button1->HasFocus()); |
| EXPECT_TRUE(button2->HasFocus()); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Widget::GetTopLevelWidget tests. |
| |
| TEST_F(WidgetTest, GetTopLevelWidget_Native) { |
| // Create a hierarchy of native widgets. |
| std::unique_ptr<Widget> toplevel = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| gfx::NativeView parent = toplevel->GetNativeView(); |
| std::unique_ptr<Widget> child = base::WrapUnique(CreateChildPlatformWidget( |
| parent, Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| EXPECT_EQ(toplevel.get(), toplevel->GetTopLevelWidget()); |
| EXPECT_EQ(toplevel.get(), child->GetTopLevelWidget()); |
| |
| // |child| should be automatically destroyed with |toplevel|. |
| } |
| |
| // Test if a focus manager and an inputmethod work without CHECK failure |
| // when window activation changes. |
| TEST_F(WidgetTest, ChangeActivation) { |
| std::unique_ptr<Widget> top1 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| top1->Show(); |
| RunPendingMessages(); |
| |
| std::unique_ptr<Widget> top2 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| top2->Show(); |
| RunPendingMessages(); |
| |
| top1->Activate(); |
| RunPendingMessages(); |
| |
| top2->Activate(); |
| RunPendingMessages(); |
| |
| top1->Activate(); |
| RunPendingMessages(); |
| } |
| |
| // Tests visibility of child widgets. |
| TEST_F(WidgetTest, Visibility) { |
| std::unique_ptr<Widget> toplevel = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| gfx::NativeView parent = toplevel->GetNativeView(); |
| std::unique_ptr<Widget> child = base::WrapUnique(CreateChildPlatformWidget( |
| parent, Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| EXPECT_FALSE(toplevel->IsVisible()); |
| EXPECT_FALSE(child->IsVisible()); |
| |
| // Showing a child with a hidden parent keeps the child hidden. |
| child->Show(); |
| EXPECT_FALSE(toplevel->IsVisible()); |
| EXPECT_FALSE(child->IsVisible()); |
| |
| // Showing a hidden parent with a visible child shows both. |
| toplevel->Show(); |
| EXPECT_TRUE(toplevel->IsVisible()); |
| EXPECT_TRUE(child->IsVisible()); |
| |
| // Hiding a parent hides both parent and child. |
| toplevel->Hide(); |
| EXPECT_FALSE(toplevel->IsVisible()); |
| EXPECT_FALSE(child->IsVisible()); |
| |
| // Hiding a child while the parent is hidden keeps the child hidden when the |
| // parent is shown. |
| child->Hide(); |
| toplevel->Show(); |
| EXPECT_TRUE(toplevel->IsVisible()); |
| EXPECT_FALSE(child->IsVisible()); |
| |
| // |child| should be automatically destroyed with |toplevel|. |
| } |
| |
| // Test that child widgets are positioned relative to their parent. |
| TEST_F(WidgetTest, ChildBoundsRelativeToParent) { |
| std::unique_ptr<Widget> toplevel = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> child = base::WrapUnique(CreateChildPlatformWidget( |
| toplevel->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| toplevel->SetBounds(gfx::Rect(160, 100, 320, 200)); |
| child->SetBounds(gfx::Rect(0, 0, 320, 200)); |
| |
| child->Show(); |
| toplevel->Show(); |
| |
| gfx::Rect toplevel_bounds = toplevel->GetWindowBoundsInScreen(); |
| |
| // Check the parent origin. If it was (0, 0) the test wouldn't be interesting. |
| EXPECT_NE(gfx::Vector2d(0, 0), toplevel_bounds.OffsetFromOrigin()); |
| |
| // The child's origin is at (0, 0), but the same size, so bounds should match. |
| EXPECT_EQ(toplevel_bounds, child->GetWindowBoundsInScreen()); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Widget ownership tests. |
| // |
| // Tests various permutations of Widget ownership specified in the |
| // InitParams::Ownership param. Make sure that they are properly destructed |
| // during shutdown. |
| |
| // A bag of state to monitor destructions. |
| struct OwnershipTestState { |
| OwnershipTestState() = default; |
| |
| bool widget_deleted = false; |
| bool native_widget_deleted = false; |
| }; |
| |
| class WidgetOwnershipTest : public WidgetTest { |
| public: |
| WidgetOwnershipTest() = default; |
| |
| WidgetOwnershipTest(const WidgetOwnershipTest&) = delete; |
| WidgetOwnershipTest& operator=(const WidgetOwnershipTest&) = delete; |
| |
| ~WidgetOwnershipTest() override = default; |
| |
| void TearDown() override { |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| WidgetTest::TearDown(); |
| } |
| |
| OwnershipTestState* state() { return &state_; } |
| |
| private: |
| OwnershipTestState state_; |
| }; |
| |
| // A Widget subclass that updates a bag of state when it is destroyed. |
| class OwnershipTestWidget : public Widget { |
| public: |
| explicit OwnershipTestWidget(OwnershipTestState* state) : state_(state) {} |
| |
| OwnershipTestWidget(const OwnershipTestWidget&) = delete; |
| OwnershipTestWidget& operator=(const OwnershipTestWidget&) = delete; |
| |
| ~OwnershipTestWidget() override { state_->widget_deleted = true; } |
| |
| private: |
| raw_ptr<OwnershipTestState> state_; |
| }; |
| |
| class NativeWidgetDestroyedWaiter { |
| public: |
| explicit NativeWidgetDestroyedWaiter(OwnershipTestState* state) |
| : state_(state) {} |
| |
| base::OnceClosure GetNativeWidgetDestroyedCallback() { |
| return base::BindOnce( |
| [](OwnershipTestState* state, base::RunLoop* run_loop) { |
| state->native_widget_deleted = true; |
| run_loop->Quit(); |
| }, |
| state_.get(), &run_loop_); |
| } |
| |
| void Wait() { |
| if (!state_->native_widget_deleted) { |
| run_loop_.Run(); |
| } |
| } |
| |
| private: |
| base::RunLoop run_loop_; |
| raw_ptr<OwnershipTestState> state_; |
| }; |
| |
| using NativeWidgetOwnsWidgetTest = WidgetOwnershipTest; |
| // NativeWidget owns its Widget, part 1.1: NativeWidget is a non-desktop |
| // widget, CloseNow() destroys Widget and NativeWidget synchronously. |
| TEST_F(NativeWidgetOwnsWidgetTest, NonDesktopWidget_CloseNow) { |
| Widget* widget = new OwnershipTestWidget(state()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget, kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| |
| widget->CloseNow(); |
| |
| // Both widget and native widget should be deleted synchronously. |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| |
| // NativeWidget owns its Widget, part 1.2: NativeWidget is a non-desktop |
| // widget, Close() destroys Widget and NativeWidget asynchronously. |
| TEST_F(NativeWidgetOwnsWidgetTest, NonDesktopWidget_Close) { |
| NativeWidgetDestroyedWaiter waiter(state()); |
| Widget* widget = new OwnershipTestWidget(state()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget, kStubCapture, waiter.GetNativeWidgetDestroyedCallback()); |
| widget->Init(std::move(params)); |
| |
| widget->Close(); |
| waiter.Wait(); |
| |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| |
| // NativeWidget owns its Widget, part 1.3: NativeWidget is a desktop |
| // widget, Close() destroys Widget and NativeWidget asynchronously. |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| TEST_F(NativeWidgetOwnsWidgetTest, DesktopWidget_Close) { |
| NativeWidgetDestroyedWaiter waiter(state()); |
| Widget* widget = new OwnershipTestWidget(state()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| params.native_widget = CreatePlatformDesktopNativeWidgetImpl( |
| widget, kStubCapture, waiter.GetNativeWidgetDestroyedCallback()); |
| widget->Init(std::move(params)); |
| |
| widget->Close(); |
| waiter.Wait(); |
| |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| #endif |
| |
| // NativeWidget owns its Widget, part 1.4: NativeWidget is a desktop |
| // widget. Unlike desktop widget, CloseNow() might destroy Widget and |
| // NativeWidget asynchronously. |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| TEST_F(NativeWidgetOwnsWidgetTest, DesktopWidget_CloseNow) { |
| NativeWidgetDestroyedWaiter waiter(state()); |
| Widget* widget = new OwnershipTestWidget(state()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| params.native_widget = CreatePlatformDesktopNativeWidgetImpl( |
| widget, kStubCapture, waiter.GetNativeWidgetDestroyedCallback()); |
| widget->Init(std::move(params)); |
| |
| widget->CloseNow(); |
| waiter.Wait(); |
| |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| #endif |
| |
| // NativeWidget owns its Widget, part 2.1: NativeWidget is a non-desktop |
| // widget. CloseNow() the parent should destroy the child. |
| TEST_F(NativeWidgetOwnsWidgetTest, NonDestkopWidget_CloseNowParent) { |
| NativeWidgetDestroyedWaiter waiter(state()); |
| Widget* toplevel = CreateTopLevelPlatformWidget(); |
| Widget* widget = new OwnershipTestWidget(state()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| params.parent = toplevel->GetNativeView(); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget, kStubCapture, waiter.GetNativeWidgetDestroyedCallback()); |
| widget->Init(std::move(params)); |
| |
| // Now destroy the native widget. This is achieved by closing the toplevel. |
| toplevel->CloseNow(); |
| // The NativeWidget won't be deleted until after a return to the message loop |
| // so we have to run pending messages before testing the destruction status. |
| waiter.Wait(); |
| |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| |
| // NativeWidget owns its Widget, part 2.2: NativeWidget is a desktop |
| // widget. CloseNow() the parent should destroy the child. |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| TEST_F(NativeWidgetOwnsWidgetTest, DestkopWidget_CloseNowParent) { |
| NativeWidgetDestroyedWaiter waiter(state()); |
| Widget* toplevel = CreateTopLevelPlatformDesktopWidget(); |
| Widget* widget = new OwnershipTestWidget(state()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| params.parent = toplevel->GetNativeView(); |
| params.native_widget = CreatePlatformDesktopNativeWidgetImpl( |
| widget, kStubCapture, waiter.GetNativeWidgetDestroyedCallback()); |
| widget->Init(std::move(params)); |
| |
| // Now destroy the native widget. This is achieved by closing the toplevel. |
| toplevel->CloseNow(); |
| // The NativeWidget won't be deleted until after a return to the message loop |
| // so we have to run pending messages before testing the destruction status. |
| waiter.Wait(); |
| |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| #endif |
| |
| // NativeWidget owns its Widget, part 3.1: NativeWidget is a non-desktop |
| // widget, destroyed out from under it by the OS. |
| TEST_F(NativeWidgetOwnsWidgetTest, NonDesktopWidget_NativeDestroy) { |
| Widget* widget = new OwnershipTestWidget(state()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget, kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| |
| // Now simulate a destroy of the platform native widget from the OS: |
| SimulateNativeDestroy(widget); |
| |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| // NativeWidget owns its Widget, part 3.2: NativeWidget is a desktop |
| // widget, destroyed out from under it by the OS. |
| TEST_F(NativeWidgetOwnsWidgetTest, DesktopWidget_NativeDestroy) { |
| NativeWidgetDestroyedWaiter waiter(state()); |
| Widget* widget = new OwnershipTestWidget(state()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| params.native_widget = CreatePlatformDesktopNativeWidgetImpl( |
| widget, kStubCapture, waiter.GetNativeWidgetDestroyedCallback()); |
| widget->Init(std::move(params)); |
| |
| // Now simulate a destroy of the platform native widget from the OS: |
| SimulateDesktopNativeDestroy(widget); |
| waiter.Wait(); |
| |
| EXPECT_TRUE(state()->widget_deleted); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| #endif |
| |
| using WidgetOwnsNativeWidgetTest = WidgetOwnershipTest; |
| // Widget owns its NativeWidget, part 1. |
| TEST_F(WidgetOwnsNativeWidgetTest, Ownership) { |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| |
| // Now delete the Widget, which should delete the NativeWidget. |
| widget.reset(); |
| |
| // TODO(beng): write test for this ownership scenario and the NativeWidget |
| // being deleted out from under the Widget. |
| } |
| |
| // Widget owns its NativeWidget, part 2: destroy the parent view. |
| TEST_F(WidgetOwnsNativeWidgetTest, DestroyParentView) { |
| Widget* toplevel = CreateTopLevelPlatformWidget(); |
| |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.parent = toplevel->GetNativeView(); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| |
| // Now close the toplevel, which deletes the view hierarchy. |
| toplevel->CloseNow(); |
| |
| RunPendingMessages(); |
| |
| // This shouldn't delete the widget because it shouldn't be deleted |
| // from the native side. |
| EXPECT_FALSE(state()->widget_deleted); |
| EXPECT_FALSE(state()->native_widget_deleted); |
| } |
| |
| // Widget owns its NativeWidget, part 3: has a WidgetDelegateView as contents. |
| TEST_F(WidgetOwnsNativeWidgetTest, WidgetDelegateView) { |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| params.delegate = new WidgetDelegateView(WidgetDelegateView::CreatePassKey()); |
| widget->Init(std::move(params)); |
| |
| // Allow the Widget to go out of scope. There should be no crash or |
| // use-after-free. |
| } |
| |
| // Widget owns its NativeWidget, part 4: Widget::CloseNow should be idempotent. |
| TEST_F(WidgetOwnsNativeWidgetTest, IdempotentCloseNow) { |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| |
| // Now close the Widget, which should delete the NativeWidget. |
| widget->CloseNow(); |
| |
| RunPendingMessages(); |
| |
| // Close the widget again should not crash. |
| widget->CloseNow(); |
| |
| RunPendingMessages(); |
| } |
| |
| // Widget owns its NativeWidget, part 5: Widget::Close should be idempotent. |
| TEST_F(WidgetOwnsNativeWidgetTest, IdempotentClose) { |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| |
| // Now close the Widget, which should delete the NativeWidget. |
| widget->Close(); |
| |
| RunPendingMessages(); |
| |
| // Close the widget again should not crash. |
| widget->Close(); |
| |
| RunPendingMessages(); |
| } |
| |
| // Tests for CLIENT_OWNS_WIDGET. The client holds a unique_ptr<Widget>. |
| // The NativeWidget will be destroyed when the platform window is closed. |
| using ClientOwnsWidgetTest = WidgetOwnershipTest; |
| |
| TEST_F(ClientOwnsWidgetTest, Ownership) { |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| |
| widget->CloseNow(); |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| |
| TEST_F(ClientOwnsWidgetTest, DestructWithAsyncCloseFirst) { |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| widget->Close(); |
| widget.reset(); |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| |
| TEST_F(ClientOwnsWidgetTest, DestructWithoutExplicitClose) { |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| widget.reset(); |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(state()->native_widget_deleted); |
| } |
| |
| class WidgetDestroyCounter : public WidgetObserver { |
| public: |
| explicit WidgetDestroyCounter(Widget* widget) |
| : widget_(widget->GetWeakPtr()) { |
| widget_->AddObserver(this); |
| } |
| ~WidgetDestroyCounter() override { |
| if (widget_) { |
| widget_->RemoveObserver(this); |
| } |
| } |
| |
| int widget_destroying_count() const { return widget_destroying_count_; } |
| |
| int widget_destroyed_count() const { return widget_destroyed_count_; } |
| |
| private: |
| // WidgetObserver: |
| void OnWidgetDestroying(Widget* widget) override { |
| ++widget_destroying_count_; |
| } |
| |
| void OnWidgetDestroyed(Widget* widget) override { ++widget_destroyed_count_; } |
| |
| base::WeakPtr<Widget> widget_; |
| int widget_destroying_count_ = 0; |
| int widget_destroyed_count_ = 0; |
| }; |
| |
| TEST_F(ClientOwnsWidgetTest, NotificationsTest) { |
| auto widget = std::make_unique<OwnershipTestWidget>(state()); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.native_widget = CreatePlatformNativeWidgetImpl( |
| widget.get(), kStubCapture, &state()->native_widget_deleted); |
| widget->Init(std::move(params)); |
| auto observer = std::make_unique<WidgetDestroyCounter>(widget.get()); |
| widget->Close(); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(observer->widget_destroying_count(), 1); |
| EXPECT_EQ(observer->widget_destroyed_count(), 1); |
| widget.reset(); |
| EXPECT_TRUE(state()->widget_deleted); |
| // The destroying & destroyed notifications should only happen once. |
| EXPECT_EQ(observer->widget_destroying_count(), 1); |
| EXPECT_EQ(observer->widget_destroyed_count(), 1); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Test to verify using various Widget methods doesn't crash when the underlying |
| // NativeView and NativeWidget is destroyed. Currently, for |
| // the WIDGET_OWNS_NATIVE_WIDGET ownership pattern, the NativeWidget will not be |
| // destroyed, but |native_widget_| will still be set to nullptr. |
| |
| class WidgetWithDestroyedNativeViewOrNativeWidgetTest |
| : public ViewsTestBase, |
| public testing::WithParamInterface< |
| std::tuple<ViewsTestBase::NativeWidgetType, |
| Widget::InitParams::Ownership>> { |
| public: |
| WidgetWithDestroyedNativeViewOrNativeWidgetTest() = default; |
| |
| WidgetWithDestroyedNativeViewOrNativeWidgetTest( |
| const WidgetWithDestroyedNativeViewOrNativeWidgetTest&) = delete; |
| WidgetWithDestroyedNativeViewOrNativeWidgetTest& operator=( |
| const WidgetWithDestroyedNativeViewOrNativeWidgetTest&) = delete; |
| |
| ~WidgetWithDestroyedNativeViewOrNativeWidgetTest() override = default; |
| |
| // ViewsTestBase: |
| void SetUp() override { |
| set_native_widget_type( |
| std::get<ViewsTestBase::NativeWidgetType>(GetParam())); |
| ViewsTestBase::SetUp(); |
| if (std::get<Widget::InitParams::Ownership>(GetParam()) == |
| Widget::InitParams::CLIENT_OWNS_WIDGET) { |
| widget_ = std::make_unique<Widget>(); |
| Widget::InitParams params = |
| CreateParamsForTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| widget_->Init(std::move(params)); |
| } else { |
| widget_ = CreateTestWidget(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| } |
| widget()->Show(); |
| widget()->native_widget_private()->CloseNow(); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| Widget* widget() { return widget_.get(); } |
| |
| static std::string PrintTestName( |
| const ::testing::TestParamInfo< |
| WidgetWithDestroyedNativeViewOrNativeWidgetTest::ParamType>& info) { |
| std::string test_name; |
| switch (std::get<ViewsTestBase::NativeWidgetType>(info.param)) { |
| case ViewsTestBase::NativeWidgetType::kDefault: |
| test_name += "DefaultNativeWidget"; |
| break; |
| case ViewsTestBase::NativeWidgetType::kDesktop: |
| test_name += "DesktopNativeWidget"; |
| break; |
| } |
| test_name += "_"; |
| switch (std::get<Widget::InitParams::Ownership>(info.param)) { |
| case Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET: |
| test_name += "WidgetOwnsNativeWidget"; |
| break; |
| case Widget::InitParams::CLIENT_OWNS_WIDGET: |
| test_name += "ClientOwnsWidget"; |
| break; |
| case Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET: |
| // Note: We don't test for this case in |
| // WidgetWithDestroyedNativeViewOrNativeWidgetTest. |
| test_name += "NativeWidgetOwnsWidget"; |
| break; |
| } |
| return test_name; |
| } |
| |
| private: |
| std::unique_ptr<Widget> widget_; |
| }; |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Activate) { |
| widget()->Activate(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, AddAndRemoveObserver) { |
| // Constructor calls |AddObserver()| |
| TestWidgetObserver observer(widget()); |
| widget()->RemoveObserver(&observer); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| AddAndRemoveRemovalsObserver) { |
| TestWidgetRemovalsObserver removals_observer; |
| widget()->AddRemovalsObserver(&removals_observer); |
| widget()->RemoveRemovalsObserver(&removals_observer); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, AsWidget) { |
| widget()->AsWidget(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, CanActivate) { |
| widget()->CanActivate(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, CenterWindow) { |
| widget()->CenterWindow(gfx::Size()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ClearNativeFocus) { |
| widget()->ClearNativeFocus(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ClientView) { |
| widget()->client_view(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Close) { |
| widget()->Close(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, CloseAllWidgets) { |
| widget()->CloseAllWidgets(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, CloseNow) { |
| widget()->CloseNow(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ClosedReason) { |
| widget()->closed_reason(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, CloseWithReason) { |
| widget()->CloseWithReason(Widget::ClosedReason::kUnspecified); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| CreateNonClientFrameView) { |
| widget()->CreateNonClientFrameView(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Deactivate) { |
| widget()->Deactivate(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, DraggedView) { |
| widget()->dragged_view(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, EndMoveLoop) { |
| widget()->EndMoveLoop(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ExecuteCommand) { |
| widget()->ExecuteCommand(0); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, FlashFrame) { |
| widget()->FlashFrame(true); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, FrameType) { |
| widget()->frame_type(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, FrameTypeChanged) { |
| widget()->FrameTypeChanged(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetAccelerator) { |
| ui::Accelerator accelerator; |
| widget()->GetAccelerator(0, &accelerator); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetAllChildWidgets) { |
| Widget::Widgets widgets = |
| Widget::GetAllChildWidgets(widget()->GetNativeView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetAllOwnedWidgets) { |
| Widget::Widgets widgets = |
| Widget::GetAllOwnedWidgets(widget()->GetNativeView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetAndSetZOrderLevel) { |
| widget()->SetZOrderLevel(ui::ZOrderLevel::kNormal); |
| widget()->GetZOrderLevel(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| GetClientAreaBoundsInScreen) { |
| widget()->GetClientAreaBoundsInScreen(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetColorProvider) { |
| widget()->GetColorProvider(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetCompositor) { |
| widget()->GetCompositor(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetContentsView) { |
| widget()->GetContentsView(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetCustomTheme) { |
| widget()->GetCustomTheme(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetEventSink) { |
| widget()->GetEventSink(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetFocusSearch) { |
| widget()->GetFocusSearch(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetFocusManager) { |
| widget()->GetFocusManager(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetFocusTraversable) { |
| widget()->GetFocusTraversable(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetGestureConsumer) { |
| widget()->GetGestureConsumer(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetGestureRecognizer) { |
| widget()->GetGestureRecognizer(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetHitTestMask) { |
| SkPath mask; |
| widget()->GetHitTestMask(&mask); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetInputMethod) { |
| widget()->GetInputMethod(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetLayer) { |
| widget()->GetLayer(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetMinimumSize) { |
| widget()->GetMinimumSize(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetMaximumSize) { |
| widget()->GetMaximumSize(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetName) { |
| widget()->GetName(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetNativeTheme) { |
| widget()->GetNativeTheme(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetNativeView) { |
| widget()->GetNativeView(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetNativeWindow) { |
| widget()->GetNativeWindow(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| GetNativeWindowProperty) { |
| widget()->GetNativeWindowProperty("xx"); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetNonClientComponent) { |
| gfx::Point point; |
| widget()->GetNonClientComponent(point); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| GetPrimaryWindowWidget) { |
| widget()->GetPrimaryWindowWidget(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetRestoredBounds) { |
| widget()->GetRestoredBounds(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetRootView) { |
| widget()->GetRootView(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetSublevelManager) { |
| widget()->GetSublevelManager(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetThemeProvider) { |
| widget()->GetThemeProvider(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetTooltipManager) { |
| widget()->GetTooltipManager(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetTopLevelWidget) { |
| widget()->GetTopLevelWidget(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| GetTopLevelWidgetForNativeView) { |
| Widget::GetTopLevelWidgetForNativeView(widget()->GetNativeView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetWeakPtr) { |
| widget()->GetWeakPtr(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| GetWidgetForNativeView) { |
| Widget::GetWidgetForNativeView(widget()->GetNativeView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| GetWidgetForNativeWindow) { |
| Widget::GetWidgetForNativeWindow(widget()->GetNativeWindow()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| GetWindowBoundsInScreen) { |
| widget()->GetWindowBoundsInScreen(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| GetWorkAreaBoundsInScreen) { |
| widget()->GetWorkAreaBoundsInScreen(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetWorkspace) { |
| widget()->GetWorkspace(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, GetZOrderSublevel) { |
| widget()->GetZOrderSublevel(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, HasCapture) { |
| widget()->HasCapture(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, HasFocusManager) { |
| widget()->HasFocusManager(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, HasHitTestMask) { |
| widget()->HasHitTestMask(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, HasObserver) { |
| TestWidgetObserver observer(widget()); |
| widget()->HasObserver(&observer); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, HasRemovalsObserver) { |
| TestWidgetRemovalsObserver observer; |
| widget()->HasRemovalsObserver(&observer); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Hide) { |
| widget()->Hide(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Init) { |
| Widget::InitParams params( |
| std::get<Widget::InitParams::Ownership>(GetParam())); |
| EXPECT_DCHECK_DEATH(widget()->Init(std::move(params))); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, is_secondary_widget) { |
| widget()->is_secondary_widget(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsActive) { |
| widget()->IsActive(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsClosed) { |
| widget()->IsClosed(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsDialogBox) { |
| widget()->IsDialogBox(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsFullscreen) { |
| widget()->IsFullscreen(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsMaximized) { |
| widget()->IsMaximized(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsMinimized) { |
| widget()->IsMinimized(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsModal) { |
| widget()->IsModal(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsMouseEventsEnabled) { |
| widget()->IsMouseEventsEnabled(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsMoveLoopSupported) { |
| widget()->IsMoveLoopSupported(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| IsNativeWidgetInitialized) { |
| widget()->IsNativeWidgetInitialized(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsStackedAbove) { |
| std::unique_ptr<Widget> other_widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| widget()->IsStackedAbove(other_widget->GetNativeView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, IsVisible) { |
| widget()->IsVisible(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| IsVisibleOnAllWorkspaces) { |
| widget()->IsVisibleOnAllWorkspaces(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnGestureEvent) { |
| ui::GestureEvent event = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollBegin, 5, 5); |
| widget()->OnGestureEvent(&event); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnKeyEvent) { |
| ui::KeyEvent event(ui::EventType::kKeyPressed, ui::VKEY_RIGHT, ui::EF_NONE); |
| widget()->OnKeyEvent(&event); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnMouseCaptureLost) { |
| widget()->OnMouseCaptureLost(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnMouseEvent) { |
| gfx::Point p(200, 200); |
| ui::MouseEvent event(ui::EventType::kMouseMoved, p, p, ui::EventTimeForNow(), |
| ui::EF_NONE, ui::EF_NONE); |
| widget()->OnMouseEvent(&event); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnNativeBlur) { |
| widget()->OnNativeBlur(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnNativeFocus) { |
| widget()->OnNativeFocus(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnNativeThemeUpdated) { |
| ui::TestNativeTheme theme; |
| widget()->OnNativeThemeUpdated(&theme); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetActivationChanged) { |
| widget()->OnNativeWidgetActivationChanged(false); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetAddedToCompositor) { |
| widget()->OnNativeWidgetAddedToCompositor(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetBeginUserBoundsChange) { |
| widget()->OnNativeWidgetBeginUserBoundsChange(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnNativeWidgetCreated) { |
| EXPECT_DCHECK_DEATH(widget()->OnNativeWidgetCreated()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetDestroyed) { |
| widget()->OnNativeWidgetDestroyed(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetDestroying) { |
| EXPECT_DCHECK_DEATH(widget()->OnNativeWidgetDestroying()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetEndUserBoundsChange) { |
| widget()->OnNativeWidgetEndUserBoundsChange(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnNativeWidgetMove) { |
| widget()->OnNativeWidgetMove(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnNativeWidgetPaint) { |
| auto display_list = base::MakeRefCounted<cc::DisplayItemList>(); |
| widget()->OnNativeWidgetPaint( |
| ui::PaintContext(display_list.get(), 1, gfx::Rect(), false)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetParentChanged) { |
| widget()->OnNativeWidgetParentChanged(gfx::NativeView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetRemovingFromCompositor) { |
| widget()->OnNativeWidgetRemovingFromCompositor(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetSizeChanged) { |
| widget()->OnNativeWidgetSizeChanged(gfx::Size()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetVisibilityChanged) { |
| widget()->OnNativeWidgetVisibilityChanged(false); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetWindowShowStateChanged) { |
| widget()->OnNativeWidgetWindowShowStateChanged(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnNativeWidgetWorkspaceChanged) { |
| widget()->OnNativeWidgetWorkspaceChanged(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnOwnerClosing) { |
| widget()->OnOwnerClosing(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnParentShouldPaintAsActiveChanged) { |
| widget()->OnParentShouldPaintAsActiveChanged(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, OnScrollEvent) { |
| ui::ScrollEvent scroll(ui::EventType::kScroll, gfx::Point(65, 5), |
| ui::EventTimeForNow(), 0, 0, 20, 0, 20, 2); |
| widget()->OnScrollEvent(&scroll); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| OnSizeConstraintsChanged) { |
| widget()->OnSizeConstraintsChanged(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, LayerTreeChanged) { |
| widget()->LayerTreeChanged(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| LayoutRootViewIfNecessary) { |
| widget()->LayoutRootViewIfNecessary(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, LockPaintAsActive) { |
| widget()->LockPaintAsActive(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Maximize) { |
| widget()->Maximize(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Minimize) { |
| widget()->Minimize(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, movement_disabled) { |
| widget()->movement_disabled(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, native_widget_private) { |
| widget()->native_widget_private(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, native_widget) { |
| widget()->native_widget(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, non_client_view) { |
| widget()->non_client_view(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| NotifyNativeViewHierarchyChanged) { |
| widget()->NotifyNativeViewHierarchyChanged(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| NotifyNativeViewHierarchyWillChange) { |
| widget()->NotifyNativeViewHierarchyWillChange(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, NotifyWillRemoveView) { |
| widget()->NotifyWillRemoveView(widget()->non_client_view()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, parent) { |
| widget()->parent(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| RegisterPaintAsActiveChangedCallback) { |
| auto subscription = |
| widget()->RegisterPaintAsActiveChangedCallback(base::DoNothing()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ReleaseCapture) { |
| widget()->ReleaseCapture(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ReorderNativeViews) { |
| widget()->ReorderNativeViews(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ReparentNativeView) { |
| EXPECT_DCHECK_DEATH( |
| Widget::ReparentNativeView(widget()->GetNativeView(), gfx::NativeView())); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Restore) { |
| widget()->Restore(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, RunMoveLoop) { |
| widget()->RunMoveLoop(gfx::Vector2d(), Widget::MoveLoopSource::kMouse, |
| Widget::MoveLoopEscapeBehavior::kHide); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, RunShellDrag) { |
| std::unique_ptr<OSExchangeData> data(std::make_unique<OSExchangeData>()); |
| widget()->RunShellDrag(nullptr, std::move(data), gfx::Point(), 0, |
| ui::mojom::DragEventSource::kMouse); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ScheduleLayout) { |
| widget()->ScheduleLayout(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SchedulePaintInRect) { |
| widget()->SchedulePaintInRect(gfx::Rect(0, 0, 1, 2)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetAspectRatio) { |
| widget()->SetAspectRatio(gfx::SizeF(1.0, 1.0)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetBounds) { |
| widget()->SetBounds(gfx::Rect(0, 0, 100, 80)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetBoundsConstrained) { |
| widget()->SetBoundsConstrained(gfx::Rect(0, 0, 120, 140)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SetCanAppearInExistingFullscreenSpaces) { |
| widget()->SetCanAppearInExistingFullscreenSpaces(false); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetCapture) { |
| widget()->SetCapture(widget()->GetRootView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetContentsView) { |
| View view; |
| EXPECT_DCHECK_DEATH(widget()->SetContentsView(std::make_unique<View>())); |
| EXPECT_DCHECK_DEATH(widget()->SetContentsView(&view)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetCursor) { |
| widget()->SetCursor(ui::Cursor()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SetFocusTraversableParent) { |
| std::unique_ptr<Widget> another_widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| widget()->SetFocusTraversableParent(another_widget->GetFocusTraversable()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SetFocusTraversableParentView) { |
| std::unique_ptr<Widget> another_widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| widget()->SetFocusTraversableParentView(another_widget->GetContentsView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetFullscreen) { |
| widget()->SetFullscreen(true); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetInitialFocus) { |
| widget()->SetInitialFocus(ui::mojom::WindowShowState::kInactive); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SetNativeWindowProperty) { |
| widget()->SetNativeWindowProperty("xx", widget()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetOpacity) { |
| widget()->SetOpacity(0.f); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetShape) { |
| auto rects = std::make_unique<Widget::ShapeRects>(); |
| rects->emplace_back(40, 0, 20, 100); |
| rects->emplace_back(0, 40, 100, 20); |
| widget()->SetShape(std::move(rects)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetSize) { |
| widget()->SetSize(gfx::Size(10, 11)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SetVisibilityChangedAnimationsEnabled) { |
| widget()->SetVisibilityChangedAnimationsEnabled(false); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SetVisibilityAnimationDuration) { |
| widget()->SetVisibilityAnimationDuration(base::Seconds(1)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SetVisibilityAnimationTransition) { |
| widget()->SetVisibilityAnimationTransition(Widget::ANIMATE_BOTH); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, SetVisible) { |
| widget()->SetVisibilityAnimationTransition(Widget::ANIMATE_BOTH); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SetVisibleOnAllWorkspaces) { |
| widget()->SetVisibleOnAllWorkspaces(true); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| ShouldDescendIntoChildForEventHandling) { |
| widget()->ShouldDescendIntoChildForEventHandling(nullptr, gfx::NativeView(), |
| nullptr, gfx::Point(0, 0)); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| ShouldHandleNativeWidgetActivationChanged) { |
| widget()->ShouldHandleNativeWidgetActivationChanged(true); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ShouldPaintAsActive) { |
| widget()->ShouldPaintAsActive(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ShouldUseNativeFrame) { |
| widget()->ShouldUseNativeFrame(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| ShouldWindowContentsBeTransparent) { |
| widget()->ShouldWindowContentsBeTransparent(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, Show) { |
| widget()->Show(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ShowEmojiPanel) { |
| widget()->ShowEmojiPanel(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ShowInactive) { |
| widget()->ShowInactive(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, StackAbove) { |
| std::unique_ptr<Widget> another_widget = |
| CreateTestWidget(std::get<Widget::InitParams::Ownership>(GetParam())); |
| widget()->StackAbove(another_widget->GetNativeView()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, StackAboveWidget) { |
| std::unique_ptr<Widget> another_widget = |
| CreateTestWidget(std::get<Widget::InitParams::Ownership>(GetParam())); |
| widget()->StackAboveWidget(another_widget.get()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, StackAtTop) { |
| widget()->StackAtTop(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| SynthesizeMouseMoveEvent) { |
| widget()->SynthesizeMouseMoveEvent(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ThemeChanged) { |
| widget()->ThemeChanged(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, UnlockPaintAsActive) { |
| // UnlockPaintAsActive() is called in the destructor of PaintAsActiveLock. |
| // External invocation is not allowed. |
| EXPECT_DCHECK_DEATH(widget()->UnlockPaintAsActive()); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, UpdateWindowIcon) { |
| widget()->UpdateWindowIcon(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, UpdateWindowTitle) { |
| widget()->UpdateWindowTitle(); |
| } |
| |
| TEST_P(WidgetWithDestroyedNativeViewOrNativeWidgetTest, ViewHierarchyChanged) { |
| widget()->ViewHierarchyChanged( |
| ViewHierarchyChangedDetails(true, nullptr, nullptr, nullptr)); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PlatformWidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| WidgetWithDestroyedNativeViewOrNativeWidgetTest, |
| ::testing::Combine( |
| ::testing::Values(ViewsTestBase::NativeWidgetType::kDefault, |
| ViewsTestBase::NativeWidgetType::kDesktop), |
| ::testing::Values(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET, |
| Widget::InitParams::CLIENT_OWNS_WIDGET)), |
| WidgetWithDestroyedNativeViewOrNativeWidgetTest::PrintTestName); |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Widget observer tests. |
| // |
| |
| class WidgetObserverTest : public WidgetTest, public WidgetObserver { |
| public: |
| WidgetObserverTest() = default; |
| ~WidgetObserverTest() override = default; |
| |
| // Set a widget to Close() the next time the Widget being observed is hidden. |
| void CloseOnNextHide(Widget* widget) { widget_to_close_on_hide_ = widget; } |
| |
| // Overridden from WidgetObserver: |
| void OnWidgetDestroying(Widget* widget) override { |
| if (active_ == widget) { |
| active_ = nullptr; |
| } |
| if (widget_activated_ == widget) { |
| widget_activated_ = nullptr; |
| } |
| } |
| |
| void OnWidgetActivationChanged(Widget* widget, bool active) override { |
| if (active) { |
| if (widget_activated_) { |
| widget_activated_->Deactivate(); |
| } |
| widget_activated_ = widget; |
| active_ = widget; |
| } else { |
| if (widget_activated_ == widget) { |
| widget_activated_ = nullptr; |
| } |
| widget_deactivated_ = widget->GetName(); |
| } |
| } |
| |
| void OnWidgetVisibilityChanged(Widget* widget, bool visible) override { |
| if (visible) { |
| widget_shown_ = widget->GetName(); |
| return; |
| } |
| widget_hidden_ = widget->GetName(); |
| if (widget_to_close_on_hide_) { |
| std::exchange(widget_to_close_on_hide_, nullptr)->Close(); |
| } |
| } |
| |
| void OnWidgetBoundsChanged(Widget* widget, |
| const gfx::Rect& new_bounds) override { |
| widget_bounds_changed_ = widget->GetName(); |
| } |
| |
| void reset() { |
| active_ = nullptr; |
| widget_activated_ = nullptr; |
| widget_deactivated_.clear(); |
| widget_shown_.clear(); |
| widget_hidden_.clear(); |
| widget_bounds_changed_.clear(); |
| } |
| |
| Widget* NewWidget(std::string name) { |
| Widget* widget = new Widget(); |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); |
| params.name = std::move(name); |
| widget->Init(std::move(params)); |
| widget->AddObserver(this); |
| return widget; |
| } |
| |
| const Widget* active() const { return active_; } |
| const Widget* widget_activated() const { return widget_activated_; } |
| const std::string& widget_deactivated() const { return widget_deactivated_; } |
| const std::string& widget_shown() const { return widget_shown_; } |
| const std::string& widget_hidden() const { return widget_hidden_; } |
| const std::string& widget_bounds_changed() const { |
| return widget_bounds_changed_; |
| } |
| |
| private: |
| raw_ptr<Widget> active_ = nullptr; |
| raw_ptr<Widget> widget_activated_ = nullptr; |
| |
| std::string widget_deactivated_; |
| std::string widget_shown_; |
| std::string widget_hidden_; |
| std::string widget_bounds_changed_; |
| |
| raw_ptr<Widget> widget_to_close_on_hide_ = nullptr; |
| }; |
| |
| // This test appears to be flaky on Mac. |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_ActivationChange DISABLED_ActivationChange |
| #else |
| #define MAYBE_ActivationChange ActivationChange |
| #endif |
| |
| TEST_F(WidgetObserverTest, MAYBE_ActivationChange) { |
| WidgetAutoclosePtr toplevel1(NewWidget("top1")); |
| WidgetAutoclosePtr toplevel2(NewWidget("top2")); |
| |
| toplevel1->Show(); |
| toplevel2->Show(); |
| reset(); |
| |
| toplevel1->Activate(); |
| RunPendingMessages(); |
| EXPECT_EQ(toplevel1.get(), widget_activated()); |
| |
| toplevel2->Activate(); |
| RunPendingMessages(); |
| EXPECT_EQ(toplevel1->GetName(), widget_deactivated()); |
| EXPECT_EQ(toplevel2.get(), widget_activated()); |
| EXPECT_EQ(toplevel2.get(), active()); |
| } |
| |
| namespace { |
| |
| // This class simulates a focus manager that moves focus to a second widget when |
| // the first one is closed. It simulates a situation where a sequence of widget |
| // observers might try to call Widget::Close in response to a OnWidgetClosing(). |
| class WidgetActivationForwarder : public TestWidgetObserver { |
| public: |
| WidgetActivationForwarder(Widget* current_active_widget, |
| Widget* widget_to_activate) |
| : TestWidgetObserver(current_active_widget), |
| widget_to_activate_(widget_to_activate) {} |
| |
| WidgetActivationForwarder(const WidgetActivationForwarder&) = delete; |
| WidgetActivationForwarder& operator=(const WidgetActivationForwarder&) = |
| delete; |
| |
| ~WidgetActivationForwarder() override = default; |
| |
| private: |
| // WidgetObserver overrides: |
| void OnWidgetClosing(Widget* widget) override { |
| widget->OnNativeWidgetActivationChanged(false); |
| widget_to_activate_->Activate(); |
| } |
| void OnWidgetActivationChanged(Widget* widget, bool active) override { |
| if (!active) { |
| widget->Close(); |
| } |
| } |
| |
| raw_ptr<Widget> widget_to_activate_; |
| }; |
| |
| // This class observes a widget and counts the number of times OnWidgetClosing |
| // is called. |
| class WidgetCloseCounter : public TestWidgetObserver { |
| public: |
| explicit WidgetCloseCounter(Widget* widget) : TestWidgetObserver(widget) {} |
| |
| WidgetCloseCounter(const WidgetCloseCounter&) = delete; |
| WidgetCloseCounter& operator=(const WidgetCloseCounter&) = delete; |
| |
| ~WidgetCloseCounter() override = default; |
| |
| int close_count() const { return close_count_; } |
| |
| private: |
| // WidgetObserver overrides: |
| void OnWidgetClosing(Widget* widget) override { close_count_++; } |
| |
| int close_count_ = 0; |
| }; |
| |
| } // namespace |
| |
| // Makes sure close notifications aren't sent more than once when a Widget is |
| // shutting down. Test for crbug.com/714334 |
| TEST_F(WidgetObserverTest, CloseReentrancy) { |
| Widget* widget1 = CreateTopLevelPlatformWidget(); |
| Widget* widget2 = CreateTopLevelPlatformWidget(); |
| WidgetCloseCounter counter(widget1); |
| WidgetActivationForwarder focus_manager(widget1, widget2); |
| widget1->Close(); |
| EXPECT_EQ(1, counter.close_count()); |
| widget2->Close(); |
| } |
| |
| TEST_F(WidgetObserverTest, VisibilityChange) { |
| WidgetAutoclosePtr toplevel(CreateTopLevelPlatformWidget()); |
| WidgetAutoclosePtr child1(NewWidget("child1")); |
| WidgetAutoclosePtr child2(NewWidget("child2")); |
| |
| toplevel->Show(); |
| child1->Show(); |
| child2->Show(); |
| |
| reset(); |
| |
| child1->Hide(); |
| EXPECT_EQ(child1->GetName(), widget_hidden()); |
| |
| child2->Hide(); |
| EXPECT_EQ(child2->GetName(), widget_hidden()); |
| |
| child1->Show(); |
| EXPECT_EQ(child1->GetName(), widget_shown()); |
| |
| child2->Show(); |
| EXPECT_EQ(child2->GetName(), widget_shown()); |
| } |
| |
| TEST_F(WidgetObserverTest, DestroyBubble) { |
| // This test expect NativeWidgetAura, force its creation. |
| ViewsDelegate::GetInstance()->set_native_widget_factory( |
| ViewsDelegate::NativeWidgetFactory()); |
| |
| std::unique_ptr<Widget> anchor = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| anchor->Show(); |
| |
| auto bubble_delegate = std::make_unique<WidgetTestBubbleDialogDelegateView>( |
| anchor->client_view()); |
| { |
| std::unique_ptr<Widget> bubble_widget = |
| base::WrapUnique(BubbleDialogDelegateView::CreateBubble( |
| bubble_delegate.release(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| bubble_widget->Show(); |
| } |
| |
| anchor->Hide(); |
| } |
| |
| TEST_F(WidgetObserverTest, WidgetBoundsChanged) { |
| WidgetAutoclosePtr child1(NewWidget("child1")); |
| WidgetAutoclosePtr child2(NewWidget("child2")); |
| |
| child1->OnNativeWidgetMove(); |
| EXPECT_EQ(child1->GetName(), widget_bounds_changed()); |
| |
| child2->OnNativeWidgetMove(); |
| EXPECT_EQ(child2->GetName(), widget_bounds_changed()); |
| |
| child1->OnNativeWidgetSizeChanged(gfx::Size()); |
| EXPECT_EQ(child1->GetName(), widget_bounds_changed()); |
| |
| child2->OnNativeWidgetSizeChanged(gfx::Size()); |
| EXPECT_EQ(child2->GetName(), widget_bounds_changed()); |
| } |
| |
| // An extension to WidgetBoundsChanged to ensure notifications are forwarded |
| // by the NativeWidget implementation. |
| TEST_F(WidgetObserverTest, WidgetBoundsChangedNative) { |
| // Don't use NewWidget(), so that the Init() flow can be observed to ensure |
| // consistency across platforms. |
| auto widget = std::make_unique<Widget>(); |
| widget->AddObserver(this); |
| |
| EXPECT_THAT(widget_bounds_changed(), IsEmpty()); |
| |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| |
| // Use an origin within the work area since platforms (e.g. Mac) may move a |
| // window into the work area when showing, triggering a bounds change. |
| params.bounds = gfx::Rect(50, 50, 100, 100); |
| params.name = "widget"; |
| |
| // Init causes a bounds change, even while not showing. Note some platforms |
| // cause a bounds change even when the bounds are empty. Mac does not. |
| widget->Init(std::move(params)); |
| EXPECT_THAT(widget_bounds_changed(), Not(IsEmpty())); |
| reset(); |
| |
| // Resizing while hidden, triggers a change. |
| widget->SetSize(gfx::Size(160, 100)); |
| EXPECT_FALSE(widget->IsVisible()); |
| EXPECT_THAT(widget_bounds_changed(), Not(IsEmpty())); |
| reset(); |
| |
| // Setting the same size does nothing. |
| widget->SetSize(gfx::Size(160, 100)); |
| EXPECT_THAT(widget_bounds_changed(), IsEmpty()); |
| reset(); |
| |
| // Showing does nothing to the bounds. |
| widget->Show(); |
| EXPECT_TRUE(widget->IsVisible()); |
| EXPECT_THAT(widget_bounds_changed(), IsEmpty()); |
| reset(); |
| |
| // Resizing while shown. |
| widget->SetSize(gfx::Size(170, 100)); |
| EXPECT_THAT(widget_bounds_changed(), Not(IsEmpty())); |
| reset(); |
| |
| // Resize to the same thing while shown does nothing. |
| widget->SetSize(gfx::Size(170, 100)); |
| EXPECT_THAT(widget_bounds_changed(), IsEmpty()); |
| reset(); |
| |
| // Move, but don't change the size. |
| widget->SetBounds(gfx::Rect(110, 110, 170, 100)); |
| EXPECT_THAT(widget_bounds_changed(), Not(IsEmpty())); |
| reset(); |
| |
| // Moving to the same place does nothing. |
| widget->SetBounds(gfx::Rect(110, 110, 170, 100)); |
| EXPECT_THAT(widget_bounds_changed(), IsEmpty()); |
| reset(); |
| |
| // No bounds change when closing. |
| std::exchange(widget, nullptr)->CloseNow(); |
| EXPECT_THAT(widget_bounds_changed(), IsEmpty()); |
| } |
| |
| namespace { |
| |
| class MoveTrackingTestDesktopWidgetDelegate : public TestDesktopWidgetDelegate { |
| public: |
| int move_count() const { return move_count_; } |
| |
| // WidgetDelegate: |
| void OnWidgetMove() override { ++move_count_; } |
| |
| private: |
| int move_count_ = 0; |
| }; |
| |
| } // namespace |
| |
| class DesktopWidgetObserverTest : public WidgetObserverTest { |
| public: |
| DesktopWidgetObserverTest() = default; |
| |
| DesktopWidgetObserverTest(const DesktopWidgetObserverTest&) = delete; |
| DesktopWidgetObserverTest& operator=(const DesktopWidgetObserverTest&) = |
| delete; |
| |
| ~DesktopWidgetObserverTest() override = default; |
| |
| // WidgetObserverTest: |
| void SetUp() override { |
| set_native_widget_type(NativeWidgetType::kDesktop); |
| WidgetObserverTest::SetUp(); |
| } |
| }; |
| |
| // An extension to the WidgetBoundsChangedNative test above to ensure move |
| // notifications propagate to the WidgetDelegate. |
| TEST_F(DesktopWidgetObserverTest, OnWidgetMovedWhenOriginChangesNative) { |
| MoveTrackingTestDesktopWidgetDelegate delegate; |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| delegate.InitWidget(std::move(params)); |
| Widget* widget = delegate.GetWidget(); |
| widget->Show(); |
| widget->SetBounds(gfx::Rect(100, 100, 300, 200)); |
| |
| const int moves_during_init = delegate.move_count(); |
| |
| // Resize without changing origin. No move. |
| widget->SetBounds(gfx::Rect(100, 100, 310, 210)); |
| EXPECT_EQ(moves_during_init, delegate.move_count()); |
| |
| // Move without changing size. Moves. |
| widget->SetBounds(gfx::Rect(110, 110, 310, 210)); |
| EXPECT_EQ(moves_during_init + 1, delegate.move_count()); |
| |
| // Changing both moves. |
| widget->SetBounds(gfx::Rect(90, 90, 330, 230)); |
| EXPECT_EQ(moves_during_init + 2, delegate.move_count()); |
| |
| // Just grow vertically. On Mac, this changes the AppKit origin since it is |
| // from the bottom left of the screen, but there is no move as far as views is |
| // concerned. |
| widget->SetBounds(gfx::Rect(90, 90, 330, 240)); |
| // No change. |
| EXPECT_EQ(moves_during_init + 2, delegate.move_count()); |
| |
| // For a similar reason, move the widget down by the same amount that it grows |
| // vertically. The AppKit origin does not change, but it is a move. |
| widget->SetBounds(gfx::Rect(90, 100, 330, 250)); |
| EXPECT_EQ(moves_during_init + 3, delegate.move_count()); |
| } |
| |
| // Test correct behavior when widgets close themselves in response to visibility |
| // changes. |
| TEST_F(WidgetObserverTest, ClosingOnHiddenParent) { |
| WidgetAutoclosePtr parent(NewWidget("parent")); |
| Widget* child = CreateChildPlatformWidget(parent->GetNativeView()); |
| |
| TestWidgetObserver child_observer(child); |
| |
| EXPECT_FALSE(parent->IsVisible()); |
| EXPECT_FALSE(child->IsVisible()); |
| |
| // Note |child| is TYPE_CONTROL, which start shown. So no need to show the |
| // child separately. |
| parent->Show(); |
| EXPECT_TRUE(parent->IsVisible()); |
| EXPECT_TRUE(child->IsVisible()); |
| |
| // Simulate a child widget that closes itself when the parent is hidden. |
| CloseOnNextHide(child); |
| EXPECT_FALSE(child_observer.widget_closed()); |
| parent->Hide(); |
| RunPendingMessages(); |
| EXPECT_TRUE(child_observer.widget_closed()); |
| } |
| |
| // Test behavior of NativeWidget*::GetWindowPlacement on the native desktop. |
| #if BUILDFLAG(IS_LINUX) |
| // On desktop-Linux cheat and use non-desktop widgets. On X11, minimize is |
| // asynchronous. Also (harder) showing a window doesn't activate it without |
| // user interaction (or extra steps only done for interactive ui tests). |
| // Without that, show_state remains in ui::mojom::WindowShowState::kInactive |
| // throughout. |
| // TODO(tapted): Find a nice way to run this with desktop widgets on Linux. |
| TEST_F(WidgetTest, GetWindowPlacement) { |
| #else |
| TEST_F(DesktopWidgetTest, GetWindowPlacement) { |
| #endif |
| WidgetAutoclosePtr widget; |
| widget.reset(CreateTopLevelNativeWidget()); |
| |
| gfx::Rect expected_bounds(100, 110, 200, 220); |
| widget->SetBounds(expected_bounds); |
| widget->Show(); |
| |
| // Start with something invalid to ensure it changes. |
| ui::mojom::WindowShowState show_state = ui::mojom::WindowShowState::kEnd; |
| gfx::Rect restored_bounds; |
| |
| internal::NativeWidgetPrivate* native_widget = |
| widget->native_widget_private(); |
| |
| native_widget->GetWindowPlacement(&restored_bounds, &show_state); |
| EXPECT_EQ(expected_bounds, restored_bounds); |
| #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) |
| // Non-desktop/Ash widgets start off in "default" until a Restore(). |
| EXPECT_EQ(ui::mojom::WindowShowState::kDefault, show_state); |
| widget->Restore(); |
| native_widget->GetWindowPlacement(&restored_bounds, &show_state); |
| #endif |
| EXPECT_EQ(ui::mojom::WindowShowState::kNormal, show_state); |
| |
| views::test::PropertyWaiter minimize_waiter( |
| base::BindRepeating(&Widget::IsMinimized, base::Unretained(widget.get())), |
| true); |
| widget->Minimize(); |
| EXPECT_TRUE(minimize_waiter.Wait()); |
| |
| native_widget->GetWindowPlacement(&restored_bounds, &show_state); |
| EXPECT_EQ(ui::mojom::WindowShowState::kMinimized, show_state); |
| EXPECT_EQ(expected_bounds, restored_bounds); |
| |
| views::test::PropertyWaiter restore_waiter( |
| base::BindRepeating(&Widget::IsMinimized, base::Unretained(widget.get())), |
| false); |
| widget->Restore(); |
| EXPECT_TRUE(restore_waiter.Wait()); |
| |
| native_widget->GetWindowPlacement(&restored_bounds, &show_state); |
| EXPECT_EQ(ui::mojom::WindowShowState::kNormal, show_state); |
| EXPECT_EQ(expected_bounds, restored_bounds); |
| |
| expected_bounds = gfx::Rect(130, 140, 230, 250); |
| widget->SetBounds(expected_bounds); |
| native_widget->GetWindowPlacement(&restored_bounds, &show_state); |
| EXPECT_EQ(ui::mojom::WindowShowState::kNormal, show_state); |
| EXPECT_EQ(expected_bounds, restored_bounds); |
| |
| widget->SetFullscreen(true); |
| native_widget->GetWindowPlacement(&restored_bounds, &show_state); |
| |
| #if BUILDFLAG(IS_WIN) |
| // Desktop Aura widgets on Windows currently don't update show_state when |
| // going fullscreen, and report restored_bounds as the full screen size. |
| // See http://crbug.com/475813. |
| EXPECT_EQ(ui::mojom::WindowShowState::kNormal, show_state); |
| #else |
| EXPECT_EQ(ui::mojom::WindowShowState::kFullscreen, show_state); |
| EXPECT_EQ(expected_bounds, restored_bounds); |
| #endif |
| |
| widget->SetFullscreen(false); |
| native_widget->GetWindowPlacement(&restored_bounds, &show_state); |
| EXPECT_EQ(ui::mojom::WindowShowState::kNormal, show_state); |
| EXPECT_EQ(expected_bounds, restored_bounds); |
| } |
| |
| // Test that widget size constraints are properly applied immediately after |
| // Init(), and that SetBounds() calls are appropriately clamped. |
| TEST_F(DesktopWidgetTest, MinimumSizeConstraints) { |
| TestDesktopWidgetDelegate delegate; |
| gfx::Size minimum_size(100, 100); |
| const gfx::Size smaller_size(90, 90); |
| |
| delegate.set_contents_view(new StaticSizedView(minimum_size)); |
| delegate.InitWidget(CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW)); |
| Widget* widget = delegate.GetWidget(); |
| |
| // On desktop Linux, the Widget must be shown to ensure the window is mapped. |
| // On other platforms this line is optional. |
| widget->Show(); |
| |
| // Sanity checks. |
| EXPECT_GT(delegate.initial_bounds().width(), minimum_size.width()); |
| EXPECT_GT(delegate.initial_bounds().height(), minimum_size.height()); |
| EXPECT_EQ(delegate.initial_bounds().size(), |
| widget->GetWindowBoundsInScreen().size()); |
| // Note: StaticSizedView doesn't currently provide a maximum size. |
| EXPECT_EQ(gfx::Size(), widget->GetMaximumSize()); |
| |
| if (!widget->ShouldUseNativeFrame()) { |
| // The test environment may have dwm disabled on Windows. In this case, |
| // DefaultFrameView is used instead of the NativeFrameView, which will |
| // provide a minimum size that includes frame decorations. |
| minimum_size = widget->non_client_view() |
| ->GetWindowBoundsForClientBounds(gfx::Rect(minimum_size)) |
| .size(); |
| } |
| |
| EXPECT_EQ(minimum_size, widget->GetMinimumSize()); |
| EXPECT_EQ(minimum_size, GetNativeWidgetMinimumContentSize(widget)); |
| |
| // Trying to resize smaller than the minimum size should restrict the content |
| // size to the minimum size. |
| widget->SetBounds(gfx::Rect(smaller_size)); |
| EXPECT_EQ(minimum_size, widget->GetClientAreaBoundsInScreen().size()); |
| |
| widget->SetSize(smaller_size); |
| EXPECT_EQ(minimum_size, widget->GetClientAreaBoundsInScreen().size()); |
| } |
| |
| // When a non-desktop widget has a desktop child widget, due to the |
| // async nature of desktop widget shutdown, the parent can be destroyed before |
| // its child. Make sure that parent() returns nullptr at this time. |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| TEST_F(DesktopWidgetTest, GetPossiblyDestroyedParent) { |
| WidgetAutoclosePtr root(CreateTopLevelNativeWidget()); |
| |
| const auto create_widget = [](Widget* parent, bool is_desktop) { |
| Widget* widget = new Widget; |
| Widget::InitParams init_params( |
| Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW); |
| init_params.parent = parent->GetNativeView(); |
| init_params.context = parent->GetNativeView(); |
| if (is_desktop) { |
| init_params.native_widget = |
| new test::TestPlatformNativeWidget<DesktopNativeWidgetAura>( |
| widget, false, nullptr); |
| } else { |
| init_params.native_widget = |
| new test::TestPlatformNativeWidget<NativeWidgetAura>(widget, false, |
| nullptr); |
| } |
| widget->Init(std::move(init_params)); |
| return widget; |
| }; |
| |
| WidgetAutoclosePtr child(create_widget(root.get(), /* non-desktop */ false)); |
| WidgetAutoclosePtr grandchild(create_widget(child.get(), /* desktop */ true)); |
| |
| child.reset(); |
| EXPECT_EQ(grandchild->parent(), nullptr); |
| } |
| #endif // BUILDFLAG(ENABLE_DESKTOP_AURA) |
| |
| // Tests that SetBounds() and GetWindowBoundsInScreen() is symmetric when the |
| // widget is visible and not maximized or fullscreen. |
| TEST_F(WidgetTest, GetWindowBoundsInScreen) { |
| // Choose test coordinates away from edges and dimensions that are "small" |
| // (but not too small) to ensure the OS doesn't try to adjust them. |
| const gfx::Rect kTestBounds(150, 150, 400, 300); |
| const gfx::Size kTestSize(200, 180); |
| |
| { |
| // First test a toplevel widget. |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->Show(); |
| |
| EXPECT_NE(kTestSize.ToString(), |
| widget->GetWindowBoundsInScreen().size().ToString()); |
| widget->SetSize(kTestSize); |
| EXPECT_EQ(kTestSize.ToString(), |
| widget->GetWindowBoundsInScreen().size().ToString()); |
| |
| EXPECT_NE(kTestBounds.ToString(), |
| widget->GetWindowBoundsInScreen().ToString()); |
| widget->SetBounds(kTestBounds); |
| EXPECT_EQ(kTestBounds.ToString(), |
| widget->GetWindowBoundsInScreen().ToString()); |
| |
| // Changing just the size should not change the origin. |
| widget->SetSize(kTestSize); |
| EXPECT_EQ(kTestBounds.origin().ToString(), |
| widget->GetWindowBoundsInScreen().origin().ToString()); |
| } |
| |
| // Same tests with a frameless window. |
| WidgetAutoclosePtr widget(CreateTopLevelFramelessPlatformWidget()); |
| widget->Show(); |
| |
| EXPECT_NE(kTestSize.ToString(), |
| widget->GetWindowBoundsInScreen().size().ToString()); |
| widget->SetSize(kTestSize); |
| EXPECT_EQ(kTestSize.ToString(), |
| widget->GetWindowBoundsInScreen().size().ToString()); |
| |
| EXPECT_NE(kTestBounds.ToString(), |
| widget->GetWindowBoundsInScreen().ToString()); |
| widget->SetBounds(kTestBounds); |
| EXPECT_EQ(kTestBounds.ToString(), |
| widget->GetWindowBoundsInScreen().ToString()); |
| |
| // For a frameless widget, the client bounds should also match. |
| EXPECT_EQ(kTestBounds.ToString(), |
| widget->GetClientAreaBoundsInScreen().ToString()); |
| |
| // Verify origin is stable for a frameless window as well. |
| widget->SetSize(kTestSize); |
| EXPECT_EQ(kTestBounds.origin().ToString(), |
| widget->GetWindowBoundsInScreen().origin().ToString()); |
| } |
| |
| // Chrome OS widgets need the shell to maximize/fullscreen window. |
| // Disable on desktop Linux because windows restore to the wrong bounds. |
| // See http://crbug.com/515369. |
| #if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX) |
| #define MAYBE_GetRestoredBounds DISABLED_GetRestoredBounds |
| #else |
| #define MAYBE_GetRestoredBounds GetRestoredBounds |
| #endif |
| |
| // Test that GetRestoredBounds() returns the original bounds of the window. |
| TEST_F(DesktopWidgetTest, MAYBE_GetRestoredBounds) { |
| WidgetAutoclosePtr toplevel(CreateTopLevelNativeWidget()); |
| toplevel->Show(); |
| // Initial restored bounds have non-zero size. |
| EXPECT_FALSE(toplevel->GetRestoredBounds().IsEmpty()); |
| |
| const gfx::Rect bounds(100, 100, 200, 200); |
| toplevel->SetBounds(bounds); |
| EXPECT_EQ(bounds, toplevel->GetWindowBoundsInScreen()); |
| EXPECT_EQ(bounds, toplevel->GetRestoredBounds()); |
| |
| toplevel->Maximize(); |
| RunPendingMessages(); |
| #if BUILDFLAG(IS_MAC) |
| // Current expectation on Mac is to do nothing on Maximize. |
| EXPECT_EQ(toplevel->GetWindowBoundsInScreen(), toplevel->GetRestoredBounds()); |
| #else |
| EXPECT_NE(toplevel->GetWindowBoundsInScreen(), toplevel->GetRestoredBounds()); |
| #endif |
| EXPECT_EQ(bounds, toplevel->GetRestoredBounds()); |
| |
| toplevel->Restore(); |
| RunPendingMessages(); |
| EXPECT_EQ(bounds, toplevel->GetWindowBoundsInScreen()); |
| EXPECT_EQ(bounds, toplevel->GetRestoredBounds()); |
| |
| toplevel->SetFullscreen(true); |
| RunPendingMessages(); |
| |
| EXPECT_NE(toplevel->GetWindowBoundsInScreen(), toplevel->GetRestoredBounds()); |
| EXPECT_EQ(bounds, toplevel->GetRestoredBounds()); |
| |
| toplevel->SetFullscreen(false); |
| RunPendingMessages(); |
| EXPECT_EQ(bounds, toplevel->GetWindowBoundsInScreen()); |
| EXPECT_EQ(bounds, toplevel->GetRestoredBounds()); |
| } |
| |
| // The key-event propagation from Widget happens differently on aura and |
| // non-aura systems because of the difference in IME. So this test works only on |
| // aura. |
| TEST_F(WidgetTest, KeyboardInputEvent) { |
| WidgetAutoclosePtr toplevel(CreateTopLevelPlatformWidget()); |
| View* container = toplevel->client_view(); |
| |
| Textfield* textfield = new Textfield(); |
| textfield->SetText(u"some text"); |
| container->AddChildViewRaw(textfield); |
| toplevel->Show(); |
| textfield->RequestFocus(); |
| |
| // The press gets handled. The release doesn't have an effect. |
| ui::KeyEvent backspace_p(ui::EventType::kKeyPressed, ui::VKEY_DELETE, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&backspace_p); |
| EXPECT_TRUE(backspace_p.stopped_propagation()); |
| ui::KeyEvent backspace_r(ui::EventType::kKeyReleased, ui::VKEY_DELETE, |
| ui::EF_NONE); |
| toplevel->OnKeyEvent(&backspace_r); |
| EXPECT_FALSE(backspace_r.handled()); |
| } |
| |
| TEST_F(WidgetTest, BubbleControlsResetOnInit) { |
| std::unique_ptr<Widget> anchor = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| anchor->Show(); |
| |
| { |
| auto bubble_delegate = std::make_unique<WidgetTestBubbleDialogDelegateView>( |
| anchor->client_view()); |
| auto* bubble_delegate_ptr = bubble_delegate.get(); |
| std::unique_ptr<Widget> bubble_widget = |
| base::WrapUnique(BubbleDialogDelegateView::CreateBubble( |
| bubble_delegate.release(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| EXPECT_TRUE(bubble_delegate_ptr->reset_controls_called_); |
| bubble_widget->Show(); |
| } |
| |
| anchor->Hide(); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| // Test to ensure that after minimize, view width is set to zero. This is only |
| // the case for desktop widgets on Windows. Other platforms retain the window |
| // size while minimized. |
| TEST_F(DesktopWidgetTest, TestViewWidthAfterMinimizingWidget) { |
| // Create a widget. |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| NonClientView* non_client_view = widget->non_client_view(); |
| non_client_view->SetFrameView(CreateMinimumSizeFrameView(widget.get())); |
| // Setting the frame view doesn't do a layout, so force one. |
| non_client_view->InvalidateLayout(); |
| views::test::RunScheduledLayout(non_client_view); |
| widget->Show(); |
| EXPECT_NE(0, non_client_view->frame_view()->width()); |
| widget->Minimize(); |
| EXPECT_EQ(0, non_client_view->frame_view()->width()); |
| } |
| #endif |
| |
| // Desktop native widget Aura tests are for non Chrome OS platforms. |
| // This class validates whether paints are received for a visible Widget. |
| // It observes Widget visibility and Close() and tracks whether subsequent |
| // paints are expected. |
| class DesktopAuraTestValidPaintWidget : public Widget, public WidgetObserver { |
| public: |
| explicit DesktopAuraTestValidPaintWidget(Widget::InitParams init_params) |
| : Widget(std::move(init_params)) { |
| observation_.Observe(this); |
| } |
| |
| DesktopAuraTestValidPaintWidget(const DesktopAuraTestValidPaintWidget&) = |
| delete; |
| DesktopAuraTestValidPaintWidget& operator=( |
| const DesktopAuraTestValidPaintWidget&) = delete; |
| |
| ~DesktopAuraTestValidPaintWidget() override = default; |
| |
| bool ReadReceivedPaintAndReset() { |
| return std::exchange(received_paint_, false); |
| } |
| |
| bool received_paint_while_hidden() const { |
| return received_paint_while_hidden_; |
| } |
| |
| void WaitUntilPaint() { |
| if (received_paint_) { |
| return; |
| } |
| base::RunLoop runloop; |
| quit_closure_ = runloop.QuitClosure(); |
| runloop.Run(); |
| quit_closure_.Reset(); |
| } |
| |
| // WidgetObserver: |
| void OnWidgetDestroying(Widget* widget) override { expect_paint_ = false; } |
| |
| void OnNativeWidgetPaint(const ui::PaintContext& context) override { |
| received_paint_ = true; |
| EXPECT_TRUE(expect_paint_); |
| if (!expect_paint_) { |
| received_paint_while_hidden_ = true; |
| } |
| Widget::OnNativeWidgetPaint(context); |
| if (!quit_closure_.is_null()) { |
| std::move(quit_closure_).Run(); |
| } |
| } |
| |
| void OnWidgetVisibilityChanged(Widget* widget, bool visible) override { |
| expect_paint_ = visible; |
| } |
| |
| private: |
| bool received_paint_ = false; |
| bool expect_paint_ = true; |
| bool received_paint_while_hidden_ = false; |
| base::OnceClosure quit_closure_; |
| base::ScopedObservation<Widget, WidgetObserver> observation_{this}; |
| }; |
| |
| namespace { |
| |
| class ContentsView : public View { |
| METADATA_HEADER(ContentsView, View) |
| public: |
| ContentsView() { |
| GetViewAccessibility().SetRole(ax::mojom::Role::kDialog); |
| GetViewAccessibility().SetName( |
| std::string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty); |
| } |
| }; |
| |
| BEGIN_METADATA(ContentsView) |
| END_METADATA |
| |
| } // namespace |
| |
| class DesktopAuraPaintWidgetTest : public DesktopWidgetTest { |
| public: |
| std::unique_ptr<DesktopAuraTestValidPaintWidget> |
| CreateDesktopAuraTestValidPaintWidget( |
| Widget::InitParams::Type type = |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS) { |
| auto widget = std::make_unique<DesktopAuraTestValidPaintWidget>( |
| CreateParamsForTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| type)); |
| |
| View* contents_view = |
| widget->SetContentsView(std::make_unique<ContentsView>()); |
| contents_view->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| |
| widget->Show(); |
| widget->Activate(); |
| |
| return widget; |
| } |
| }; |
| |
| TEST_F(DesktopAuraPaintWidgetTest, DesktopNativeWidgetNoPaintAfterCloseTest) { |
| auto widget = CreateDesktopAuraTestValidPaintWidget(); |
| widget->WaitUntilPaint(); |
| EXPECT_TRUE(widget->ReadReceivedPaintAndReset()); |
| widget->SchedulePaintInRect(widget->GetRestoredBounds()); |
| widget->Close(); |
| RunPendingMessages(); |
| EXPECT_FALSE(widget->ReadReceivedPaintAndReset()); |
| EXPECT_FALSE(widget->received_paint_while_hidden()); |
| } |
| |
| TEST_F(DesktopAuraPaintWidgetTest, DesktopNativeWidgetNoPaintAfterHideTest) { |
| auto widget = CreateDesktopAuraTestValidPaintWidget(); |
| widget->WaitUntilPaint(); |
| EXPECT_TRUE(widget->ReadReceivedPaintAndReset()); |
| widget->SchedulePaintInRect(widget->GetRestoredBounds()); |
| widget->Hide(); |
| RunPendingMessages(); |
| EXPECT_FALSE(widget->ReadReceivedPaintAndReset()); |
| EXPECT_FALSE(widget->received_paint_while_hidden()); |
| widget->Close(); |
| } |
| |
| // Test to ensure that the aura Window's visibility state is set to visible if |
| // the underlying widget is hidden and then shown. |
| TEST_F(DesktopWidgetTest, TestWindowVisibilityAfterHide) { |
| // Create a widget. |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| NonClientView* non_client_view = widget->non_client_view(); |
| non_client_view->SetFrameView(CreateMinimumSizeFrameView(widget.get())); |
| |
| widget->Show(); |
| EXPECT_TRUE(IsNativeWindowVisible(widget->GetNativeWindow())); |
| widget->Hide(); |
| EXPECT_FALSE(IsNativeWindowVisible(widget->GetNativeWindow())); |
| widget->Show(); |
| EXPECT_TRUE(IsNativeWindowVisible(widget->GetNativeWindow())); |
| } |
| |
| // Tests that wheel events generated from scroll events are targeted to the |
| // views under the cursor when the focused view does not processed them. |
| TEST_F(WidgetTest, WheelEventsFromScrollEventTarget) { |
| EventCountView* cursor_view = new EventCountView; |
| cursor_view->SetBounds(60, 0, 50, 40); |
| |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->GetRootView()->AddChildViewRaw(cursor_view); |
| |
| // Generate a scroll event on the cursor view. |
| ui::ScrollEvent scroll(ui::EventType::kScroll, gfx::Point(65, 5), |
| ui::EventTimeForNow(), 0, 0, 20, 0, 20, 2); |
| widget->OnScrollEvent(&scroll); |
| |
| EXPECT_EQ(1, cursor_view->GetEventCount(ui::EventType::kScroll)); |
| EXPECT_EQ(1, cursor_view->GetEventCount(ui::EventType::kMousewheel)); |
| |
| cursor_view->ResetCounts(); |
| |
| ui::ScrollEvent scroll2(ui::EventType::kScroll, gfx::Point(5, 5), |
| ui::EventTimeForNow(), 0, 0, 20, 0, 20, 2); |
| widget->OnScrollEvent(&scroll2); |
| |
| EXPECT_EQ(0, cursor_view->GetEventCount(ui::EventType::kScroll)); |
| EXPECT_EQ(0, cursor_view->GetEventCount(ui::EventType::kMousewheel)); |
| } |
| |
| // Tests that if a scroll-begin gesture is not handled, then subsequent scroll |
| // events are not dispatched to any view. |
| TEST_F(WidgetTest, GestureScrollEventDispatching) { |
| EventCountView* noscroll_view = new EventCountView; |
| EventCountView* scroll_view = new ScrollableEventCountView; |
| |
| noscroll_view->SetBounds(0, 0, 50, 40); |
| scroll_view->SetBounds(60, 0, 40, 40); |
| |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->GetRootView()->AddChildViewRaw(noscroll_view); |
| widget->GetRootView()->AddChildViewRaw(scroll_view); |
| |
| { |
| ui::GestureEvent begin = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollBegin, 5, 5); |
| widget->OnGestureEvent(&begin); |
| ui::GestureEvent update = CreateTestGestureEvent( |
| ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate, 20, 10), |
| 25, 15); |
| widget->OnGestureEvent(&update); |
| ui::GestureEvent end = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollEnd, 25, 15); |
| widget->OnGestureEvent(&end); |
| |
| EXPECT_EQ(1, |
| noscroll_view->GetEventCount(ui::EventType::kGestureScrollBegin)); |
| EXPECT_EQ( |
| 0, noscroll_view->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(0, |
| noscroll_view->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| } |
| |
| { |
| ui::GestureEvent begin = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollBegin, 65, 5); |
| widget->OnGestureEvent(&begin); |
| ui::GestureEvent update = CreateTestGestureEvent( |
| ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate, 20, 10), |
| 85, 15); |
| widget->OnGestureEvent(&update); |
| ui::GestureEvent end = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollEnd, 85, 15); |
| widget->OnGestureEvent(&end); |
| |
| EXPECT_EQ(1, |
| scroll_view->GetEventCount(ui::EventType::kGestureScrollBegin)); |
| EXPECT_EQ(1, |
| scroll_view->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(1, scroll_view->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| } |
| } |
| |
| // Tests that event-handlers installed on the RootView get triggered correctly. |
| // TODO(tdanderson): Clean up this test as part of crbug.com/355680. |
| TEST_F(WidgetTest, EventHandlersOnRootView) { |
| WidgetAutoclosePtr widget(CreateTopLevelNativeWidget()); |
| View* root_view = widget->GetRootView(); |
| |
| EventCountView* view = |
| root_view->AddChildView(std::make_unique<EventCountView>()); |
| view->SetBounds(0, 0, 20, 20); |
| |
| EventCountHandler h1; |
| root_view->AddPreTargetHandler(&h1); |
| |
| EventCountHandler h2; |
| root_view->AddPostTargetHandler(&h2); |
| |
| widget->SetBounds(gfx::Rect(0, 0, 100, 100)); |
| widget->Show(); |
| |
| // Dispatch a ui::EventType::kScroll event. The event remains unhandled and |
| // should bubble up the views hierarchy to be re-dispatched on the root view. |
| ui::ScrollEvent scroll(ui::EventType::kScroll, gfx::Point(5, 5), |
| ui::EventTimeForNow(), 0, 0, 20, 0, 20, 2); |
| widget->OnScrollEvent(&scroll); |
| EXPECT_EQ(2, h1.GetEventCount(ui::EventType::kScroll)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kScroll)); |
| EXPECT_EQ(2, h2.GetEventCount(ui::EventType::kScroll)); |
| |
| // Unhandled scroll events are turned into wheel events and re-dispatched. |
| EXPECT_EQ(1, h1.GetEventCount(ui::EventType::kMousewheel)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMousewheel)); |
| EXPECT_EQ(1, h2.GetEventCount(ui::EventType::kMousewheel)); |
| |
| h1.ResetCounts(); |
| view->ResetCounts(); |
| h2.ResetCounts(); |
| |
| // Dispatch a ui::EventType::kScrollFlingStart event. The event remains |
| // unhandled and should bubble up the views hierarchy to be re-dispatched on |
| // the root view. |
| ui::ScrollEvent fling(ui::EventType::kScrollFlingStart, gfx::Point(5, 5), |
| ui::EventTimeForNow(), 0, 0, 20, 0, 20, 2); |
| widget->OnScrollEvent(&fling); |
| EXPECT_EQ(2, h1.GetEventCount(ui::EventType::kScrollFlingStart)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kScrollFlingStart)); |
| EXPECT_EQ(2, h2.GetEventCount(ui::EventType::kScrollFlingStart)); |
| |
| // Unhandled scroll events which are not of type ui::EventType::kScroll should |
| // not be turned into wheel events and re-dispatched. |
| EXPECT_EQ(0, h1.GetEventCount(ui::EventType::kMousewheel)); |
| EXPECT_EQ(0, view->GetEventCount(ui::EventType::kMousewheel)); |
| EXPECT_EQ(0, h2.GetEventCount(ui::EventType::kMousewheel)); |
| |
| h1.ResetCounts(); |
| view->ResetCounts(); |
| h2.ResetCounts(); |
| |
| // Change the handle mode of |view| so that events are marked as handled at |
| // the target phase. |
| view->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| |
| // Dispatch a ui::EventType::kGestureTapDown and a |
| // ui::EventType::kGestureTapCancel event. The events are handled at the |
| // target phase and should not reach the post-target handler. |
| ui::GestureEvent tap_down = |
| CreateTestGestureEvent(ui::EventType::kGestureTapDown, 5, 5); |
| widget->OnGestureEvent(&tap_down); |
| EXPECT_EQ(1, h1.GetEventCount(ui::EventType::kGestureTapDown)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kGestureTapDown)); |
| EXPECT_EQ(0, h2.GetEventCount(ui::EventType::kGestureTapDown)); |
| |
| ui::GestureEvent tap_cancel = |
| CreateTestGestureEvent(ui::EventType::kGestureTapCancel, 5, 5); |
| widget->OnGestureEvent(&tap_cancel); |
| EXPECT_EQ(1, h1.GetEventCount(ui::EventType::kGestureTapCancel)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kGestureTapCancel)); |
| EXPECT_EQ(0, h2.GetEventCount(ui::EventType::kGestureTapCancel)); |
| |
| h1.ResetCounts(); |
| view->ResetCounts(); |
| h2.ResetCounts(); |
| |
| // Dispatch a ui::EventType::kScroll event. The event is handled at the target |
| // phase and should not reach the post-target handler. |
| ui::ScrollEvent consumed_scroll(ui::EventType::kScroll, gfx::Point(5, 5), |
| ui::EventTimeForNow(), 0, 0, 20, 0, 20, 2); |
| widget->OnScrollEvent(&consumed_scroll); |
| EXPECT_EQ(1, h1.GetEventCount(ui::EventType::kScroll)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kScroll)); |
| EXPECT_EQ(0, h2.GetEventCount(ui::EventType::kScroll)); |
| |
| // Handled scroll events are not turned into wheel events and re-dispatched. |
| EXPECT_EQ(0, h1.GetEventCount(ui::EventType::kMousewheel)); |
| EXPECT_EQ(0, view->GetEventCount(ui::EventType::kMousewheel)); |
| EXPECT_EQ(0, h2.GetEventCount(ui::EventType::kMousewheel)); |
| |
| root_view->RemovePreTargetHandler(&h1); |
| } |
| |
| TEST_F(WidgetTest, SynthesizeMouseMoveEvent) { |
| WidgetAutoclosePtr widget(CreateTopLevelNativeWidget()); |
| View* root_view = widget->GetRootView(); |
| widget->SetBounds(gfx::Rect(0, 0, 100, 100)); |
| |
| EventCountView* v1 = |
| root_view->AddChildView(std::make_unique<EventCountView>()); |
| v1->SetBounds(5, 5, 10, 10); |
| EventCountView* v2 = |
| root_view->AddChildView(std::make_unique<EventCountView>()); |
| v2->SetBounds(5, 15, 10, 10); |
| |
| widget->Show(); |
| |
| // SynthesizeMouseMoveEvent does nothing until the mouse is entered. |
| widget->SynthesizeMouseMoveEvent(); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseMoved)); |
| |
| gfx::Point cursor_location(v1->GetBoundsInScreen().CenterPoint()); |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| generator->MoveMouseTo(cursor_location); |
| |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseMoved)); |
| |
| // SynthesizeMouseMoveEvent dispatches an mousemove event. |
| widget->SynthesizeMouseMoveEvent(); |
| EXPECT_EQ(2, v1->GetEventCount(ui::EventType::kMouseMoved)); |
| |
| root_view->RemoveChildViewT(v1); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseMoved)); |
| v2->SetBounds(5, 5, 10, 10); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseMoved)); |
| |
| widget->SynthesizeMouseMoveEvent(); |
| EXPECT_EQ(1, v2->GetEventCount(ui::EventType::kMouseMoved)); |
| } |
| |
| namespace { |
| |
| // ui::EventHandler which handles all mouse press events. |
| class MousePressEventConsumer : public ui::EventHandler { |
| public: |
| MousePressEventConsumer() = default; |
| |
| MousePressEventConsumer(const MousePressEventConsumer&) = delete; |
| MousePressEventConsumer& operator=(const MousePressEventConsumer&) = delete; |
| |
| private: |
| // ui::EventHandler: |
| void OnMouseEvent(ui::MouseEvent* event) override { |
| if (event->type() == ui::EventType::kMousePressed) { |
| event->SetHandled(); |
| } |
| } |
| }; |
| |
| } // namespace |
| |
| // No touch on desktop Mac. Tracked in http://crbug.com/445520. |
| #if !BUILDFLAG(IS_MAC) || defined(USE_AURA) |
| |
| // Test that mouse presses and mouse releases are dispatched normally when a |
| // touch is down. |
| TEST_F(WidgetTest, MouseEventDispatchWhileTouchIsDown) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->Show(); |
| widget->SetSize(gfx::Size(300, 300)); |
| |
| EventCountView* event_count_view = |
| widget->GetRootView()->AddChildView(std::make_unique<EventCountView>()); |
| event_count_view->SetBounds(0, 0, 300, 300); |
| |
| MousePressEventConsumer consumer; |
| event_count_view->AddPostTargetHandler(&consumer); |
| |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| generator->PressTouch(); |
| generator->ClickLeftButton(); |
| |
| EXPECT_EQ(1, event_count_view->GetEventCount(ui::EventType::kMousePressed)); |
| EXPECT_EQ(1, event_count_view->GetEventCount(ui::EventType::kMouseReleased)); |
| |
| // For mus it's important we destroy the widget before the EventGenerator. |
| widget->CloseNow(); |
| } |
| |
| #endif // !BUILDFLAG(IS_MAC) || defined(USE_AURA) |
| |
| // Tests that when there is no active capture, that a mouse press causes capture |
| // to be set. |
| TEST_F(WidgetTest, MousePressCausesCapture) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->Show(); |
| widget->SetSize(gfx::Size(300, 300)); |
| |
| EventCountView* event_count_view = |
| widget->GetRootView()->AddChildView(std::make_unique<EventCountView>()); |
| event_count_view->SetBounds(0, 0, 300, 300); |
| |
| // No capture has been set. |
| EXPECT_EQ(gfx::NativeView(), internal::NativeWidgetPrivate::GetGlobalCapture( |
| widget->GetNativeView())); |
| |
| MousePressEventConsumer consumer; |
| event_count_view->AddPostTargetHandler(&consumer); |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| generator->MoveMouseTo(widget->GetClientAreaBoundsInScreen().CenterPoint()); |
| generator->PressLeftButton(); |
| |
| EXPECT_EQ(1, event_count_view->GetEventCount(ui::EventType::kMousePressed)); |
| EXPECT_EQ( |
| widget->GetNativeView(), |
| internal::NativeWidgetPrivate::GetGlobalCapture(widget->GetNativeView())); |
| |
| // For mus it's important we destroy the widget before the EventGenerator. |
| widget->CloseNow(); |
| } |
| |
| namespace { |
| |
| // An EventHandler which shows a Widget upon receiving a mouse event. The Widget |
| // proceeds to take capture. |
| class CaptureEventConsumer : public ui::EventHandler { |
| public: |
| explicit CaptureEventConsumer(Widget* widget) : widget_(widget) {} |
| |
| CaptureEventConsumer(const CaptureEventConsumer&) = delete; |
| CaptureEventConsumer& operator=(const CaptureEventConsumer&) = delete; |
| |
| ~CaptureEventConsumer() override { widget_.ExtractAsDangling()->CloseNow(); } |
| |
| private: |
| // ui::EventHandler: |
| void OnMouseEvent(ui::MouseEvent* event) override { |
| if (event->type() == ui::EventType::kMousePressed) { |
| event->SetHandled(); |
| widget_->Show(); |
| widget_->SetSize(gfx::Size(200, 200)); |
| |
| auto event_count_view = std::make_unique<EventCountView>(); |
| event_count_view->SetBounds(0, 0, 200, 200); |
| widget_->SetCapture( |
| widget_->GetRootView()->AddChildView(std::move(event_count_view))); |
| } |
| } |
| |
| raw_ptr<Widget> widget_; |
| }; |
| |
| } // namespace |
| |
| // Tests that if explicit capture occurs during a mouse press, that implicit |
| // capture is not applied. |
| TEST_F(WidgetTest, CaptureDuringMousePressNotOverridden) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->Show(); |
| widget->SetSize(gfx::Size(300, 300)); |
| |
| EventCountView* event_count_view = |
| widget->GetRootView()->AddChildView(std::make_unique<EventCountView>()); |
| event_count_view->SetBounds(0, 0, 300, 300); |
| |
| EXPECT_EQ(gfx::NativeView(), internal::NativeWidgetPrivate::GetGlobalCapture( |
| widget->GetNativeView())); |
| |
| Widget* widget2 = CreateTopLevelNativeWidget(); |
| // Gives explicit capture to |widget2| |
| CaptureEventConsumer consumer(widget2); |
| event_count_view->AddPostTargetHandler(&consumer); |
| auto generator = |
| CreateEventGenerator(GetRootWindow(widget), widget->GetNativeWindow()); |
| generator->MoveMouseTo(widget->GetClientAreaBoundsInScreen().CenterPoint()); |
| // This event should implicitly give capture to |widget|, except that |
| // |consumer| will explicitly set capture on |widget2|. |
| generator->PressLeftButton(); |
| |
| EXPECT_EQ(1, event_count_view->GetEventCount(ui::EventType::kMousePressed)); |
| EXPECT_NE( |
| widget->GetNativeView(), |
| internal::NativeWidgetPrivate::GetGlobalCapture(widget->GetNativeView())); |
| EXPECT_EQ( |
| widget2->GetNativeView(), |
| internal::NativeWidgetPrivate::GetGlobalCapture(widget->GetNativeView())); |
| |
| // For mus it's important we destroy the widget before the EventGenerator. |
| widget->CloseNow(); |
| } |
| |
| class ClosingEventObserver : public ui::EventObserver { |
| public: |
| explicit ClosingEventObserver(Widget* widget) : widget_(widget) {} |
| |
| ClosingEventObserver(const ClosingEventObserver&) = delete; |
| ClosingEventObserver& operator=(const ClosingEventObserver&) = delete; |
| |
| // ui::EventObserver: |
| void OnEvent(const ui::Event& event) override { |
| // Guard against attempting to close the widget twice. |
| if (widget_) { |
| widget_.ExtractAsDangling()->CloseNow(); |
| } |
| } |
| |
| private: |
| raw_ptr<Widget, DanglingUntriaged> widget_; |
| }; |
| |
| class ClosingView : public View { |
| METADATA_HEADER(ClosingView, View) |
| |
| public: |
| explicit ClosingView(Widget* widget) : widget_(widget) {} |
| |
| ClosingView(const ClosingView&) = delete; |
| ClosingView& operator=(const ClosingView&) = delete; |
| |
| // View: |
| void OnEvent(ui::Event* event) override { |
| // Guard against closing twice and writing to freed memory. |
| if (widget_ && event->type() == ui::EventType::kMousePressed) { |
| Widget* widget = widget_; |
| widget_ = nullptr; |
| widget->CloseNow(); |
| } |
| } |
| |
| private: |
| raw_ptr<Widget> widget_; |
| }; |
| |
| BEGIN_METADATA(ClosingView) |
| END_METADATA |
| |
| // Ensures that when multiple objects are intercepting OS-level events, that one |
| // can safely close a Widget that has capture. |
| TEST_F(WidgetTest, DestroyedWithCaptureViaEventMonitor) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| TestWidgetObserver observer(widget); |
| widget->Show(); |
| widget->SetSize(gfx::Size(300, 300)); |
| |
| // ClosingView and ClosingEventObserver both try to close the Widget. On Mac |
| // the order that EventMonitors receive OS events is not deterministic. If the |
| // one installed via SetCapture() sees it first, the event is swallowed (so |
| // both need to try). Note the regression test would only fail when the |
| // SetCapture() handler did _not_ swallow the event, but it still needs to try |
| // to close the Widget otherwise it will be left open, which fails elsewhere. |
| ClosingView* closing_view = widget->GetContentsView()->AddChildView( |
| std::make_unique<ClosingView>(widget)); |
| widget->SetCapture(closing_view); |
| |
| ClosingEventObserver closing_event_observer(widget); |
| auto monitor = EventMonitor::CreateApplicationMonitor( |
| &closing_event_observer, widget->GetNativeWindow(), |
| {ui::EventType::kMousePressed}); |
| |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| generator->set_target(ui::test::EventGenerator::Target::APPLICATION); |
| |
| EXPECT_FALSE(observer.widget_closed()); |
| generator->PressLeftButton(); |
| EXPECT_TRUE(observer.widget_closed()); |
| } |
| |
| TEST_F(WidgetTest, LockPaintAsActive) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->ShowInactive(); |
| EXPECT_FALSE(widget->ShouldPaintAsActive()); |
| |
| // First lock causes widget to paint as active. |
| auto lock = widget->LockPaintAsActive(); |
| EXPECT_TRUE(widget->ShouldPaintAsActive()); |
| |
| // Second lock has no effect. |
| auto lock2 = widget->LockPaintAsActive(); |
| EXPECT_TRUE(widget->ShouldPaintAsActive()); |
| |
| // Have to release twice to get back to inactive state. |
| lock2.reset(); |
| EXPECT_TRUE(widget->ShouldPaintAsActive()); |
| lock.reset(); |
| EXPECT_FALSE(widget->ShouldPaintAsActive()); |
| } |
| |
| TEST_F(WidgetTest, LockPaintAsActive_AlreadyActive) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->Show(); |
| EXPECT_TRUE(widget->ShouldPaintAsActive()); |
| |
| // Lock has no effect. |
| auto lock = widget->LockPaintAsActive(); |
| EXPECT_TRUE(widget->ShouldPaintAsActive()); |
| |
| // Remove lock has no effect. |
| lock.reset(); |
| EXPECT_TRUE(widget->ShouldPaintAsActive()); |
| } |
| |
| TEST_F(WidgetTest, LockPaintAsActive_BecomesActive) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->ShowInactive(); |
| EXPECT_FALSE(widget->ShouldPaintAsActive()); |
| |
| // Lock toggles render mode. |
| auto lock = widget->LockPaintAsActive(); |
| EXPECT_TRUE(widget->ShouldPaintAsActive()); |
| |
| widget->Activate(); |
| |
| // Remove lock has no effect. |
| lock.reset(); |
| EXPECT_TRUE(widget->ShouldPaintAsActive()); |
| } |
| |
| class PaintAsActiveCallbackCounter { |
| public: |
| explicit PaintAsActiveCallbackCounter(Widget* widget) { |
| // Subscribe to |widget|'s paint-as-active change. |
| paint_as_active_subscription_ = |
| widget->RegisterPaintAsActiveChangedCallback(base::BindRepeating( |
| &PaintAsActiveCallbackCounter::Call, base::Unretained(this))); |
| } |
| void Call() { count_++; } |
| int CallCount() { return count_; } |
| |
| private: |
| int count_ = 0; |
| base::CallbackListSubscription paint_as_active_subscription_; |
| }; |
| |
| TEST_F(WidgetTest, LockParentPaintAsActive) { |
| if constexpr (!PlatformStyle::kInactiveWidgetControlsAppearDisabled) { |
| return; |
| } |
| |
| WidgetAutoclosePtr parent(CreateTopLevelPlatformWidget()); |
| WidgetAutoclosePtr child(CreateChildPlatformWidget(parent->GetNativeView())); |
| WidgetAutoclosePtr grandchild( |
| CreateChildPlatformWidget(child->GetNativeView())); |
| WidgetAutoclosePtr other(CreateTopLevelPlatformWidget()); |
| child->widget_delegate()->SetCanActivate(true); |
| grandchild->widget_delegate()->SetCanActivate(true); |
| |
| PaintAsActiveCallbackCounter parent_control(parent.get()); |
| PaintAsActiveCallbackCounter child_control(child.get()); |
| PaintAsActiveCallbackCounter grandchild_control(grandchild.get()); |
| PaintAsActiveCallbackCounter other_control(other.get()); |
| |
| parent->Show(); |
| EXPECT_TRUE(parent->ShouldPaintAsActive()); |
| EXPECT_TRUE(child->ShouldPaintAsActive()); |
| EXPECT_TRUE(grandchild->ShouldPaintAsActive()); |
| EXPECT_FALSE(other->ShouldPaintAsActive()); |
| EXPECT_EQ(parent_control.CallCount(), 1); |
| EXPECT_EQ(child_control.CallCount(), 1); |
| EXPECT_EQ(grandchild_control.CallCount(), 1); |
| EXPECT_EQ(other_control.CallCount(), 0); |
| |
| other->Show(); |
| EXPECT_FALSE(parent->ShouldPaintAsActive()); |
| EXPECT_FALSE(child->ShouldPaintAsActive()); |
| EXPECT_FALSE(grandchild->ShouldPaintAsActive()); |
| EXPECT_TRUE(other->ShouldPaintAsActive()); |
| EXPECT_EQ(parent_control.CallCount(), 2); |
| EXPECT_EQ(child_control.CallCount(), 2); |
| EXPECT_EQ(grandchild_control.CallCount(), 2); |
| EXPECT_EQ(other_control.CallCount(), 1); |
| |
| child->Show(); |
| EXPECT_TRUE(parent->ShouldPaintAsActive()); |
| EXPECT_TRUE(child->ShouldPaintAsActive()); |
| EXPECT_TRUE(grandchild->ShouldPaintAsActive()); |
| EXPECT_FALSE(other->ShouldPaintAsActive()); |
| EXPECT_EQ(parent_control.CallCount(), 3); |
| EXPECT_EQ(child_control.CallCount(), 3); |
| EXPECT_EQ(grandchild_control.CallCount(), 3); |
| EXPECT_EQ(other_control.CallCount(), 2); |
| |
| other->Show(); |
| EXPECT_FALSE(parent->ShouldPaintAsActive()); |
| EXPECT_FALSE(child->ShouldPaintAsActive()); |
| EXPECT_FALSE(grandchild->ShouldPaintAsActive()); |
| EXPECT_TRUE(other->ShouldPaintAsActive()); |
| EXPECT_EQ(parent_control.CallCount(), 4); |
| EXPECT_EQ(child_control.CallCount(), 4); |
| EXPECT_EQ(grandchild_control.CallCount(), 4); |
| EXPECT_EQ(other_control.CallCount(), 3); |
| |
| grandchild->Show(); |
| EXPECT_TRUE(parent->ShouldPaintAsActive()); |
| EXPECT_TRUE(child->ShouldPaintAsActive()); |
| EXPECT_TRUE(grandchild->ShouldPaintAsActive()); |
| EXPECT_FALSE(other->ShouldPaintAsActive()); |
| EXPECT_EQ(parent_control.CallCount(), 5); |
| EXPECT_EQ(child_control.CallCount(), 5); |
| EXPECT_EQ(grandchild_control.CallCount(), 5); |
| EXPECT_EQ(other_control.CallCount(), 4); |
| } |
| |
| // Tests to make sure that child widgets do not cause their parent widget to |
| // paint inactive immediately when they are closed. This avoids having the |
| // parent paint as inactive in the time between when the bubble is closed and |
| // when it's eventually destroyed by its native widget (see crbug.com/1303549). |
| TEST_F(DesktopWidgetTest, |
| ClosingActiveChildDoesNotPrematurelyPaintParentInactive) { |
| // top_level_widget that owns the bubble widget. |
| auto top_level_widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| top_level_widget->Show(); |
| |
| // Create the child bubble widget. |
| auto bubble_widget = std::make_unique<Widget>(); |
| Widget::InitParams init_params = CreateParamsForTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_BUBBLE); |
| init_params.parent = top_level_widget->GetNativeView(); |
| bubble_widget->Init(std::move(init_params)); |
| bubble_widget->Show(); |
| |
| EXPECT_TRUE(bubble_widget->ShouldPaintAsActive()); |
| EXPECT_TRUE(top_level_widget->ShouldPaintAsActive()); |
| |
| // Closing the bubble wiget should not immediately cause the top level widget |
| // to paint inactive. |
| PaintAsActiveCallbackCounter top_level_counter(top_level_widget.get()); |
| PaintAsActiveCallbackCounter bubble_counter(bubble_widget.get()); |
| bubble_widget->Close(); |
| EXPECT_FALSE(bubble_widget->ShouldPaintAsActive()); |
| EXPECT_TRUE(top_level_widget->ShouldPaintAsActive()); |
| EXPECT_EQ(top_level_counter.CallCount(), 0); |
| EXPECT_EQ(bubble_counter.CallCount(), 0); |
| } |
| |
| // Tests that there is no crash when paint as active lock is removed for child |
| // widget while its parent widget is being closed. |
| TEST_F(DesktopWidgetTest, LockPaintAsActiveAndCloseParent) { |
| // Make sure that DesktopNativeWidgetAura is used for widgets. |
| test_views_delegate()->set_use_desktop_native_widgets(true); |
| |
| std::unique_ptr<Widget> parent = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| parent->Show(); |
| |
| auto delegate = std::make_unique<TestDesktopWidgetDelegate>(); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| params.parent = parent->GetNativeView(); |
| delegate->InitWidget(std::move(params)); |
| delegate->RegisterDeleteDelegateCallback( |
| WidgetDelegate::RegisterDeleteCallbackPassKey(), |
| base::DoNothingWithBoundArgs(delegate->GetWidget()->LockPaintAsActive())); |
| base::WeakPtr<Widget> child = delegate->GetWidget()->GetWeakPtr(); |
| child->ShowInactive(); |
| |
| // Child widget and its delegate are destroyed when the parent widget is being |
| // closed. PaintAsActiveTestDesktopWidgetDelegate::paint_as_active_lock_ is |
| // also deleted which should not cause a crash. |
| parent->CloseNow(); |
| |
| // Ensure that child widget has been destroyed. |
| ASSERT_TRUE(child && child->IsClosed()); |
| } |
| |
| // Widget used to destroy itself when OnNativeWidgetDestroyed is called. |
| class TestNativeWidgetDestroyedWidget : public Widget { |
| public: |
| // Overridden from NativeWidgetDelegate: |
| void OnNativeWidgetDestroyed() override; |
| }; |
| |
| void TestNativeWidgetDestroyedWidget::OnNativeWidgetDestroyed() { |
| Widget::OnNativeWidgetDestroyed(); |
| delete this; |
| } |
| |
| // Verifies that widget destroyed itself in OnNativeWidgetDestroyed does not |
| // crash in ASan. |
| TEST_F(DesktopWidgetTest, WidgetDestroyedItselfDoesNotCrash) { |
| TestDesktopWidgetDelegate delegate(new TestNativeWidgetDestroyedWidget); |
| delegate.InitWidget( |
| CreateParamsForTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS)); |
| delegate.GetWidget()->Show(); |
| delegate.GetWidget()->CloseNow(); |
| } |
| |
| // Verifies WindowClosing() is invoked correctly on the delegate when a Widget |
| // is closed. |
| TEST_F(DesktopWidgetTest, SingleWindowClosing) { |
| TestDesktopWidgetDelegate delegate; |
| delegate.InitWidget(CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW)); |
| EXPECT_EQ(0, delegate.window_closing_count()); |
| delegate.GetWidget()->CloseNow(); |
| EXPECT_EQ(1, delegate.window_closing_count()); |
| } |
| |
| TEST_F(DesktopWidgetTest, CloseRequested_AllowsClose) { |
| constexpr Widget::ClosedReason kReason = Widget::ClosedReason::kLostFocus; |
| TestDesktopWidgetDelegate delegate; |
| delegate.set_can_close(true); |
| delegate.InitWidget(CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW)); |
| WidgetDestroyedWaiter waiter(delegate.GetWidget()); |
| |
| delegate.GetWidget()->CloseWithReason(kReason); |
| EXPECT_TRUE(delegate.GetWidget()->IsClosed()); |
| EXPECT_EQ(kReason, delegate.GetWidget()->closed_reason()); |
| EXPECT_EQ(kReason, delegate.last_closed_reason()); |
| |
| waiter.Wait(); |
| } |
| |
| TEST_F(DesktopWidgetTest, CloseRequested_DisallowClose) { |
| constexpr Widget::ClosedReason kReason = Widget::ClosedReason::kLostFocus; |
| TestDesktopWidgetDelegate delegate; |
| delegate.set_can_close(false); |
| delegate.InitWidget(CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW)); |
| |
| delegate.GetWidget()->CloseWithReason(kReason); |
| EXPECT_FALSE(delegate.GetWidget()->IsClosed()); |
| EXPECT_EQ(Widget::ClosedReason::kUnspecified, |
| delegate.GetWidget()->closed_reason()); |
| EXPECT_EQ(kReason, delegate.last_closed_reason()); |
| |
| delegate.GetWidget()->CloseNow(); |
| } |
| |
| TEST_F(DesktopWidgetTest, CloseRequested_SecondCloseIgnored) { |
| constexpr Widget::ClosedReason kReason1 = Widget::ClosedReason::kLostFocus; |
| constexpr Widget::ClosedReason kReason2 = Widget::ClosedReason::kUnspecified; |
| TestDesktopWidgetDelegate delegate; |
| delegate.set_can_close(true); |
| delegate.InitWidget(CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW)); |
| WidgetDestroyedWaiter waiter(delegate.GetWidget()); |
| |
| // Close for the first time. |
| delegate.GetWidget()->CloseWithReason(kReason1); |
| EXPECT_TRUE(delegate.GetWidget()->IsClosed()); |
| EXPECT_EQ(kReason1, delegate.last_closed_reason()); |
| |
| // Calling close again should have no effect. |
| delegate.GetWidget()->CloseWithReason(kReason2); |
| EXPECT_TRUE(delegate.GetWidget()->IsClosed()); |
| EXPECT_EQ(kReason1, delegate.last_closed_reason()); |
| |
| waiter.Wait(); |
| } |
| |
| class WidgetWindowTitleTest : public DesktopWidgetTest { |
| protected: |
| void RunTest(bool desktop_native_widget) { |
| auto widget = std::make_unique<Widget>(); |
| Widget::InitParams init_params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW); |
| |
| if (!desktop_native_widget) { |
| init_params.native_widget = |
| CreatePlatformNativeWidgetImpl(widget.get(), kStubCapture, nullptr); |
| } |
| widget->Init(std::move(init_params)); |
| |
| internal::NativeWidgetPrivate* native_widget = |
| widget->native_widget_private(); |
| |
| std::u16string empty; |
| std::u16string s1(u"Title1"); |
| std::u16string s2(u"Title2"); |
| std::u16string s3(u"TitleLong"); |
| |
| // The widget starts with no title, setting empty should not change |
| // anything. |
| EXPECT_FALSE(native_widget->SetWindowTitle(empty)); |
| // Setting the title to something non-empty should cause a change. |
| EXPECT_TRUE(native_widget->SetWindowTitle(s1)); |
| // Setting the title to something else with the same length should cause a |
| // change. |
| EXPECT_TRUE(native_widget->SetWindowTitle(s2)); |
| // Setting the title to something else with a different length should cause |
| // a change. |
| EXPECT_TRUE(native_widget->SetWindowTitle(s3)); |
| // Setting the title to the same thing twice should not cause a change. |
| EXPECT_FALSE(native_widget->SetWindowTitle(s3)); |
| } |
| }; |
| |
| TEST_F(WidgetWindowTitleTest, SetWindowTitleChanged_NativeWidget) { |
| // Use the default NativeWidget. |
| bool desktop_native_widget = false; |
| RunTest(desktop_native_widget); |
| } |
| |
| TEST_F(WidgetWindowTitleTest, SetWindowTitleChanged_DesktopNativeWidget) { |
| // Override to use a DesktopNativeWidget. |
| bool desktop_native_widget = true; |
| RunTest(desktop_native_widget); |
| } |
| |
| TEST_F(WidgetTest, WidgetDeleted_InOnMousePressed) { |
| Widget* widget = new Widget; |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| widget->Init(std::move(params)); |
| |
| widget->SetContentsView( |
| std::make_unique<CloseWidgetView>(ui::EventType::kMousePressed)); |
| |
| widget->SetSize(gfx::Size(100, 100)); |
| widget->Show(); |
| |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| |
| WidgetDeletionObserver deletion_observer(widget); |
| generator->PressLeftButton(); |
| if (deletion_observer.IsWidgetAlive()) { |
| generator->ReleaseLeftButton(); |
| } |
| EXPECT_FALSE(deletion_observer.IsWidgetAlive()); |
| |
| // Yay we did not crash! |
| } |
| |
| // No touch on desktop Mac. Tracked in http://crbug.com/445520. |
| #if !BUILDFLAG(IS_MAC) || defined(USE_AURA) |
| |
| TEST_F(WidgetTest, WidgetDeleted_InDispatchGestureEvent) { |
| Widget* widget = new Widget; |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP); |
| widget->Init(std::move(params)); |
| |
| widget->SetContentsView( |
| std::make_unique<CloseWidgetView>(ui::EventType::kGestureTapDown)); |
| |
| widget->SetSize(gfx::Size(100, 100)); |
| widget->Show(); |
| |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| |
| WidgetDeletionObserver deletion_observer(widget); |
| generator->GestureTapAt(widget->GetWindowBoundsInScreen().CenterPoint()); |
| EXPECT_FALSE(deletion_observer.IsWidgetAlive()); |
| |
| // Yay we did not crash! |
| } |
| |
| #endif // !BUILDFLAG(IS_MAC) || defined(USE_AURA) |
| |
| // See description of RunGetNativeThemeFromDestructor() for details. |
| class GetNativeThemeFromDestructorView : public WidgetDelegateView { |
| public: |
| GetNativeThemeFromDestructorView() = default; |
| |
| GetNativeThemeFromDestructorView(const GetNativeThemeFromDestructorView&) = |
| delete; |
| GetNativeThemeFromDestructorView& operator=( |
| const GetNativeThemeFromDestructorView&) = delete; |
| |
| ~GetNativeThemeFromDestructorView() override { VerifyNativeTheme(); } |
| |
| private: |
| void VerifyNativeTheme() { ASSERT_TRUE(GetNativeTheme() != nullptr); } |
| }; |
| |
| // Verifies GetNativeTheme() from the destructor of a WidgetDelegateView doesn't |
| // crash. |is_first_run| is true if this is the first call. A return value of |
| // true indicates this should be run again with a value of false. |
| // First run uses DesktopNativeWidgetAura (if possible). Second run doesn't. |
| bool RunGetNativeThemeFromDestructor(Widget::InitParams params, |
| bool is_first_run) { |
| bool needs_second_run = false; |
| // Destroyed by CloseNow() below. |
| WidgetTest::WidgetAutoclosePtr widget(new Widget); |
| // Deletes itself when the Widget is destroyed. |
| params.delegate = new GetNativeThemeFromDestructorView; |
| if (!is_first_run) { |
| params.native_widget = |
| CreatePlatformNativeWidgetImpl(widget.get(), kStubCapture, nullptr); |
| needs_second_run = true; |
| } |
| widget->Init(std::move(params)); |
| return needs_second_run; |
| } |
| |
| // See description of RunGetNativeThemeFromDestructor() for details. |
| TEST_F(DesktopWidgetTest, DISABLED_GetNativeThemeFromDestructor) { |
| if (RunGetNativeThemeFromDestructor( |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP), |
| true)) { |
| RunGetNativeThemeFromDestructor( |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP), |
| false); |
| } |
| } |
| |
| // Used by HideCloseDestroy. Allows setting a boolean when the widget is |
| // destroyed. |
| class CloseDestroysWidget : public Widget { |
| public: |
| CloseDestroysWidget(bool* destroyed, base::OnceClosure quit_closure) |
| : destroyed_(destroyed), quit_closure_(std::move(quit_closure)) { |
| DCHECK(destroyed_); |
| DCHECK(quit_closure_); |
| } |
| |
| CloseDestroysWidget(const CloseDestroysWidget&) = delete; |
| CloseDestroysWidget& operator=(const CloseDestroysWidget&) = delete; |
| |
| ~CloseDestroysWidget() override { |
| *destroyed_ = true; |
| std::move(quit_closure_).Run(); |
| } |
| |
| void Detach() { destroyed_ = nullptr; } |
| |
| private: |
| raw_ptr<bool> destroyed_; |
| base::OnceClosure quit_closure_; |
| }; |
| |
| // An observer that registers that an animation has ended. |
| class AnimationEndObserver : public ui::ImplicitAnimationObserver { |
| public: |
| AnimationEndObserver() = default; |
| |
| AnimationEndObserver(const AnimationEndObserver&) = delete; |
| AnimationEndObserver& operator=(const AnimationEndObserver&) = delete; |
| |
| ~AnimationEndObserver() override = default; |
| |
| bool animation_completed() const { return animation_completed_; } |
| |
| // ui::ImplicitAnimationObserver: |
| void OnImplicitAnimationsCompleted() override { animation_completed_ = true; } |
| |
| private: |
| bool animation_completed_ = false; |
| }; |
| |
| // An observer that registers the bounds of a widget on destruction. |
| class WidgetBoundsObserver : public WidgetObserver { |
| public: |
| explicit WidgetBoundsObserver(Widget* widget) { |
| widget_observation_.Observe(widget); |
| } |
| |
| WidgetBoundsObserver(const WidgetBoundsObserver&) = delete; |
| WidgetBoundsObserver& operator=(const WidgetBoundsObserver&) = delete; |
| |
| ~WidgetBoundsObserver() override = default; |
| |
| gfx::Rect bounds() { return bounds_; } |
| |
| // WidgetObserver: |
| void OnWidgetDestroying(Widget* widget) override { |
| EXPECT_TRUE(widget->GetNativeWindow()); |
| EXPECT_TRUE(Widget::GetWidgetForNativeWindow(widget->GetNativeWindow())); |
| bounds_ = widget->GetWindowBoundsInScreen(); |
| widget_observation_.Reset(); |
| } |
| |
| private: |
| gfx::Rect bounds_; |
| base::ScopedObservation<Widget, WidgetObserver> widget_observation_{this}; |
| }; |
| |
| // Verifies Close() results in destroying. |
| TEST_F(DesktopWidgetTest, CloseDestroys) { |
| bool destroyed = false; |
| base::RunLoop run_loop; |
| CloseDestroysWidget* widget = |
| new CloseDestroysWidget(&destroyed, run_loop.QuitClosure()); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_MENU); |
| params.opacity = Widget::InitParams::WindowOpacity::kOpaque; |
| params.bounds = gfx::Rect(50, 50, 250, 250); |
| widget->Init(std::move(params)); |
| widget->Show(); |
| widget->Hide(); |
| widget->Close(); |
| EXPECT_FALSE(destroyed); |
| // Run the message loop as Close() asynchronously deletes. |
| run_loop.Run(); |
| EXPECT_TRUE(destroyed); |
| // Close() should destroy the widget. If not we'll cleanup to avoid leaks. |
| if (!destroyed) { |
| widget->Detach(); |
| widget->CloseNow(); |
| } |
| } |
| |
| // Tests that killing a widget while animating it does not crash. |
| TEST_F(WidgetTest, CloseWidgetWhileAnimating) { |
| std::unique_ptr<Widget> widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| AnimationEndObserver animation_observer; |
| WidgetBoundsObserver widget_observer(widget.get()); |
| gfx::Rect bounds(100, 100, 50, 50); |
| { |
| // Normal animations for tests have ZERO_DURATION, make sure we are actually |
| // animating the movement. |
| ui::ScopedAnimationDurationScaleMode animation_scale_mode( |
| ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); |
| ui::ScopedLayerAnimationSettings animation_settings( |
| widget->GetLayer()->GetAnimator()); |
| animation_settings.AddObserver(&animation_observer); |
| widget->Show(); |
| |
| // Animate the bounds change. |
| widget->SetBounds(bounds); |
| widget->CloseNow(); |
| widget.reset(); |
| EXPECT_FALSE(animation_observer.animation_completed()); |
| } |
| EXPECT_TRUE(animation_observer.animation_completed()); |
| EXPECT_EQ(widget_observer.bounds(), bounds); |
| } |
| |
| // Test Widget::CloseAllWidgets works as expected across platforms. ChromeOS |
| // doesn't implement or need CloseAllWidgets() since everything is under a |
| // single root window. |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| TEST_F(DesktopWidgetTest, CloseAllWidgets) { |
| Widget* widget1 = CreateTopLevelNativeWidget(); |
| Widget* widget2 = CreateTopLevelNativeWidget(); |
| TestWidgetObserver observer1(widget1); |
| TestWidgetObserver observer2(widget2); |
| widget1->Show(); // Just show the first one. |
| Widget::CloseAllWidgets(); |
| EXPECT_TRUE(observer1.widget_closed()); |
| EXPECT_TRUE(observer2.widget_closed()); |
| } |
| #endif |
| |
| // Test that the NativeWidget is still valid during OnNativeWidgetDestroying(), |
| // and properties that depend on it are valid, when closed via CloseNow(). |
| TEST_F(DesktopWidgetTest, ValidDuringOnNativeWidgetDestroyingFromCloseNow) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->Show(); |
| gfx::Rect screen_rect(50, 50, 100, 100); |
| widget->SetBounds(screen_rect); |
| WidgetBoundsObserver observer(widget); |
| widget->CloseNow(); |
| EXPECT_EQ(screen_rect, observer.bounds()); |
| } |
| |
| // Test that the NativeWidget is still valid during OnNativeWidgetDestroying(), |
| // and properties that depend on it are valid, when closed via Close(). |
| TEST_F(DesktopWidgetTest, ValidDuringOnNativeWidgetDestroyingFromClose) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->Show(); |
| gfx::Rect screen_rect(50, 50, 100, 100); |
| widget->SetBounds(screen_rect); |
| WidgetBoundsObserver observer(widget); |
| widget->Close(); |
| EXPECT_EQ(gfx::Rect(), observer.bounds()); |
| base::RunLoop().RunUntilIdle(); |
| // Broken on Linux. See http://crbug.com/515379. |
| #if !BUILDFLAG(IS_LINUX) |
| EXPECT_EQ(screen_rect, observer.bounds()); |
| #endif |
| } |
| |
| // Tests that we do not crash when a Widget is destroyed by going out of |
| // scope (as opposed to being explicitly deleted by its NativeWidget). |
| TEST_F(WidgetTest, NoCrashOnWidgetDelete) { |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| } |
| |
| TEST_F(WidgetTest, NoCrashOnResizeConstraintsWindowTitleOnPopup) { |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_POPUP) |
| ->OnSizeConstraintsChanged(); |
| } |
| |
| // Tests that we do not crash when a Widget is destroyed before it finishes |
| // processing of pending input events in the message loop. |
| TEST_F(WidgetTest, NoCrashOnWidgetDeleteWithPendingEvents) { |
| std::unique_ptr<Widget> widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| widget->Show(); |
| |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| generator->MoveMouseTo(10, 10); |
| |
| // No touch on desktop Mac. Tracked in http://crbug.com/445520. |
| #if BUILDFLAG(IS_MAC) |
| generator->ClickLeftButton(); |
| #else |
| generator->PressTouch(); |
| #endif |
| widget.reset(); |
| } |
| |
| // A view that consumes mouse-pressed event and gesture-tap-down events. |
| class RootViewTestView : public View { |
| METADATA_HEADER(RootViewTestView, View) |
| |
| public: |
| RootViewTestView() = default; |
| |
| private: |
| bool OnMousePressed(const ui::MouseEvent& event) override { return true; } |
| |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| if (event->type() == ui::EventType::kGestureTapDown) { |
| event->SetHandled(); |
| } |
| } |
| }; |
| |
| BEGIN_METADATA(RootViewTestView) |
| END_METADATA |
| |
| // Checks if RootView::*_handler_ fields are unset when widget is hidden. |
| // Fails on chromium.webkit Windows bot, see crbug.com/264872. |
| #if BUILDFLAG(IS_WIN) |
| #define MAYBE_DisableTestRootViewHandlersWhenHidden \ |
| DISABLED_TestRootViewHandlersWhenHidden |
| #else |
| #define MAYBE_DisableTestRootViewHandlersWhenHidden \ |
| TestRootViewHandlersWhenHidden |
| #endif |
| TEST_F(WidgetTest, MAYBE_DisableTestRootViewHandlersWhenHidden) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->SetBounds(gfx::Rect(0, 0, 300, 300)); |
| View* view = new RootViewTestView(); |
| view->SetBounds(0, 0, 300, 300); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget->GetRootView()); |
| root_view->AddChildViewRaw(view); |
| |
| // Check RootView::mouse_pressed_handler_. |
| widget->Show(); |
| EXPECT_EQ(nullptr, GetMousePressedHandler(root_view)); |
| gfx::Point click_location(45, 15); |
| ui::MouseEvent press(ui::EventType::kMousePressed, click_location, |
| click_location, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); |
| widget->OnMouseEvent(&press); |
| EXPECT_EQ(view, GetMousePressedHandler(root_view)); |
| widget->Hide(); |
| EXPECT_EQ(nullptr, GetMousePressedHandler(root_view)); |
| |
| // Check RootView::mouse_move_handler_. |
| widget->Show(); |
| EXPECT_EQ(nullptr, GetMouseMoveHandler(root_view)); |
| gfx::Point move_location(45, 15); |
| ui::MouseEvent move(ui::EventType::kMouseMoved, move_location, move_location, |
| ui::EventTimeForNow(), 0, 0); |
| widget->OnMouseEvent(&move); |
| EXPECT_EQ(view, GetMouseMoveHandler(root_view)); |
| widget->Hide(); |
| EXPECT_EQ(nullptr, GetMouseMoveHandler(root_view)); |
| |
| // Check RootView::gesture_handler_. |
| widget->Show(); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| ui::GestureEvent tap_down = |
| CreateTestGestureEvent(ui::EventType::kGestureTapDown, 15, 15); |
| widget->OnGestureEvent(&tap_down); |
| EXPECT_EQ(view, GetGestureHandler(root_view)); |
| widget->Hide(); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| |
| widget->Close(); |
| } |
| |
| // Tests that the |gesture_handler_| member in RootView is always NULL |
| // after the dispatch of a ui::EventType::kGestureEnd event corresponding to |
| // the release of the final touch point on the screen, but that |
| // ui::EventType::kGestureEnd events corresponding to the removal of any other |
| // touch point do not modify |gesture_handler_|. |
| TEST_F(WidgetTest, GestureEndEvents) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->SetBounds(gfx::Rect(0, 0, 300, 300)); |
| EventCountView* view = new EventCountView(); |
| view->SetBounds(0, 0, 300, 300); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget->GetRootView()); |
| root_view->AddChildViewRaw(view); |
| widget->Show(); |
| |
| // If no gesture handler is set, a ui::EventType::kGestureEnd event should not |
| // set the gesture handler. |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| ui::GestureEvent end = |
| CreateTestGestureEvent(ui::EventType::kGestureEnd, 15, 15); |
| widget->OnGestureEvent(&end); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| |
| // Change the handle mode of |view| to indicate that it would like |
| // to handle all events, then send a GESTURE_TAP to set the gesture handler. |
| view->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| ui::GestureEvent tap = |
| CreateTestGestureEvent(ui::EventType::kGestureTap, 15, 15); |
| widget->OnGestureEvent(&tap); |
| EXPECT_TRUE(tap.handled()); |
| EXPECT_EQ(view, GetGestureHandler(root_view)); |
| |
| // The gesture handler should remain unchanged on a ui::EventType::kGestureEnd |
| // corresponding to a second touch point, but should be reset to NULL by a |
| // ui::EventType::kGestureEnd corresponding to the final touch point. |
| ui::GestureEventDetails details(ui::EventType::kGestureEnd); |
| details.set_touch_points(2); |
| ui::GestureEvent end_second_touch_point = |
| CreateTestGestureEvent(details, 15, 15); |
| widget->OnGestureEvent(&end_second_touch_point); |
| EXPECT_EQ(view, GetGestureHandler(root_view)); |
| |
| end = CreateTestGestureEvent(ui::EventType::kGestureEnd, 15, 15); |
| widget->OnGestureEvent(&end); |
| EXPECT_TRUE(end.handled()); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| |
| // Send a GESTURE_TAP to set the gesture handler, then change the handle |
| // mode of |view| to indicate that it does not want to handle any |
| // further events. |
| tap = CreateTestGestureEvent(ui::EventType::kGestureTap, 15, 15); |
| widget->OnGestureEvent(&tap); |
| EXPECT_TRUE(tap.handled()); |
| EXPECT_EQ(view, GetGestureHandler(root_view)); |
| view->set_handle_mode(EventCountView::HandleMode::kPropagateEvents); |
| |
| // The gesture handler should remain unchanged on a ui::EventType::kGestureEnd |
| // corresponding to a second touch point, but should be reset to NULL by a |
| // ui::EventType::kGestureEnd corresponding to the final touch point. |
| end_second_touch_point = CreateTestGestureEvent(details, 15, 15); |
| widget->OnGestureEvent(&end_second_touch_point); |
| EXPECT_EQ(view, GetGestureHandler(root_view)); |
| |
| end = CreateTestGestureEvent(ui::EventType::kGestureEnd, 15, 15); |
| widget->OnGestureEvent(&end); |
| EXPECT_FALSE(end.handled()); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| |
| widget->Close(); |
| } |
| |
| // Tests that gesture events which should not be processed (because |
| // RootView::OnEventProcessingStarted() has marked them as handled) are not |
| // dispatched to any views. |
| TEST_F(WidgetTest, GestureEventsNotProcessed) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->SetBounds(gfx::Rect(0, 0, 300, 300)); |
| |
| // Define a hierarchy of four views (coordinates are in |
| // their parent coordinate space). |
| // v1 (0, 0, 300, 300) |
| // v2 (0, 0, 100, 100) |
| // v3 (0, 0, 50, 50) |
| // v4(0, 0, 10, 10) |
| EventCountView* v1 = new EventCountView(); |
| v1->SetBounds(0, 0, 300, 300); |
| EventCountView* v2 = new EventCountView(); |
| v2->SetBounds(0, 0, 100, 100); |
| EventCountView* v3 = new EventCountView(); |
| v3->SetBounds(0, 0, 50, 50); |
| EventCountView* v4 = new EventCountView(); |
| v4->SetBounds(0, 0, 10, 10); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget->GetRootView()); |
| root_view->AddChildViewRaw(v1); |
| v1->AddChildViewRaw(v2); |
| v2->AddChildViewRaw(v3); |
| v3->AddChildViewRaw(v4); |
| |
| widget->Show(); |
| |
| // ui::EventType::kGestureBegin events should never be seen by any view, but |
| // they should be marked as handled by OnEventProcessingStarted(). |
| ui::GestureEvent begin = |
| CreateTestGestureEvent(ui::EventType::kGestureBegin, 5, 5); |
| widget->OnGestureEvent(&begin); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureBegin)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureBegin)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGestureBegin)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureBegin)); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| EXPECT_TRUE(begin.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // ui::EventType::kGestureEnd events should not be seen by any view when there |
| // is no default gesture handler set, but they should be marked as handled by |
| // OnEventProcessingStarted(). |
| ui::GestureEvent end = |
| CreateTestGestureEvent(ui::EventType::kGestureEnd, 5, 5); |
| widget->OnGestureEvent(&end); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| EXPECT_TRUE(end.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // ui::EventType::kGestureEnd events not corresponding to the release of the |
| // final touch point should never be seen by any view, but they should |
| // be marked as handled by OnEventProcessingStarted(). |
| ui::GestureEventDetails details(ui::EventType::kGestureEnd); |
| details.set_touch_points(2); |
| ui::GestureEvent end_second_touch_point = |
| CreateTestGestureEvent(details, 5, 5); |
| widget->OnGestureEvent(&end_second_touch_point); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| EXPECT_TRUE(end_second_touch_point.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // ui::EventType::kGestureScrollUpdate events should never be seen by any view |
| // when there is no default gesture handler set, but they should be marked as |
| // handled by OnEventProcessingStarted(). |
| ui::GestureEvent scroll_update = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollUpdate, 5, 5); |
| widget->OnGestureEvent(&scroll_update); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| EXPECT_TRUE(scroll_update.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // ui::EventType::kGestureScrollEnd events should never be seen by any view |
| // when there is no default gesture handler set, but they should be marked as |
| // handled by OnEventProcessingStarted(). |
| ui::GestureEvent scroll_end = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollEnd, 5, 5); |
| widget->OnGestureEvent(&scroll_end); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| EXPECT_TRUE(scroll_end.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // ui::EventType::kScrollFlingStart events should never be seen by any view |
| // when there is no default gesture handler set, but they should be marked as |
| // handled by OnEventProcessingStarted(). |
| ui::GestureEvent scroll_fling_start = |
| CreateTestGestureEvent(ui::EventType::kScrollFlingStart, 5, 5); |
| widget->OnGestureEvent(&scroll_fling_start); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kScrollFlingStart)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kScrollFlingStart)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kScrollFlingStart)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kScrollFlingStart)); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| EXPECT_TRUE(scroll_fling_start.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| widget->Close(); |
| } |
| |
| // Tests that a (non-scroll) gesture event is dispatched to the correct views |
| // in a view hierarchy and that the default gesture handler in RootView is set |
| // correctly. |
| TEST_F(WidgetTest, GestureEventDispatch) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->SetBounds(gfx::Rect(0, 0, 300, 300)); |
| |
| // Define a hierarchy of four views (coordinates are in |
| // their parent coordinate space). |
| // v1 (0, 0, 300, 300) |
| // v2 (0, 0, 100, 100) |
| // v3 (0, 0, 50, 50) |
| // v4(0, 0, 10, 10) |
| EventCountView* v1 = new EventCountView(); |
| v1->SetBounds(0, 0, 300, 300); |
| EventCountView* v2 = new EventCountView(); |
| v2->SetBounds(0, 0, 100, 100); |
| EventCountView* v3 = new EventCountView(); |
| v3->SetBounds(0, 0, 50, 50); |
| EventCountView* v4 = new EventCountView(); |
| v4->SetBounds(0, 0, 10, 10); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget->GetRootView()); |
| root_view->AddChildViewRaw(v1); |
| v1->AddChildViewRaw(v2); |
| v2->AddChildViewRaw(v3); |
| v3->AddChildViewRaw(v4); |
| |
| widget->Show(); |
| |
| // No gesture handler is set in the root view and none of the views in the |
| // view hierarchy handle a ui::EventType::kGestureTap event. In this case the |
| // tap event should be dispatched to all views in the hierarchy, the gesture |
| // handler should remain unset, and the event should remain unhandled. |
| ui::GestureEvent tap = |
| CreateTestGestureEvent(ui::EventType::kGestureTap, 5, 5); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| widget->OnGestureEvent(&tap); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(1, v2->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(1, v4->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| EXPECT_FALSE(tap.handled()); |
| |
| // No gesture handler is set in the root view and |v1|, |v2|, and |v3| all |
| // handle a ui::EventType::kGestureTap event. In this case the tap event |
| // should be dispatched to |v4| and |v3|, the gesture handler should be set to |
| // |v3|, and the event should be marked as handled. |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| v1->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| v2->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| v3->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| tap = CreateTestGestureEvent(ui::EventType::kGestureTap, 5, 5); |
| widget->OnGestureEvent(&tap); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(1, v4->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(v3, GetGestureHandler(root_view)); |
| EXPECT_TRUE(tap.handled()); |
| |
| // The gesture handler is set to |v3| and all views handle all gesture event |
| // types. In this case subsequent gesture events should only be dispatched to |
| // |v3| and marked as handled. The gesture handler should remain as |v3|. |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| v4->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| tap = CreateTestGestureEvent(ui::EventType::kGestureTap, 5, 5); |
| widget->OnGestureEvent(&tap); |
| EXPECT_TRUE(tap.handled()); |
| ui::GestureEvent show_press = |
| CreateTestGestureEvent(ui::EventType::kGestureShowPress, 5, 5); |
| widget->OnGestureEvent(&show_press); |
| tap = CreateTestGestureEvent(ui::EventType::kGestureTap, 5, 5); |
| widget->OnGestureEvent(&tap); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(2, v3->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureShowPress)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureShowPress)); |
| EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kGestureShowPress)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureShowPress)); |
| EXPECT_TRUE(tap.handled()); |
| EXPECT_TRUE(show_press.handled()); |
| EXPECT_EQ(v3, GetGestureHandler(root_view)); |
| |
| // The gesture handler is set to |v3|, but |v3| does not handle |
| // ui::EventType::kGestureTap events. In this case a tap gesture should be |
| // dispatched only to |v3|, but the event should remain unhandled. The gesture |
| // handler should remain as |v3|. |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| v3->set_handle_mode(EventCountView::HandleMode::kPropagateEvents); |
| tap = CreateTestGestureEvent(ui::EventType::kGestureTap, 5, 5); |
| widget->OnGestureEvent(&tap); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_FALSE(tap.handled()); |
| EXPECT_EQ(v3, GetGestureHandler(root_view)); |
| |
| widget->Close(); |
| } |
| |
| // Tests that gesture scroll events will change the default gesture handler in |
| // RootView if the current handler to which they are dispatched does not handle |
| // gesture scroll events. |
| TEST_F(WidgetTest, ScrollGestureEventDispatch) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->SetBounds(gfx::Rect(0, 0, 300, 300)); |
| |
| // Define a hierarchy of four views (coordinates are in |
| // their parent coordinate space). |
| // v1 (0, 0, 300, 300) |
| // v2 (0, 0, 100, 100) |
| // v3 (0, 0, 50, 50) |
| // v4(0, 0, 10, 10) |
| EventCountView* v1 = new EventCountView(); |
| v1->SetBounds(0, 0, 300, 300); |
| EventCountView* v2 = new EventCountView(); |
| v2->SetBounds(0, 0, 100, 100); |
| EventCountView* v3 = new EventCountView(); |
| v3->SetBounds(0, 0, 50, 50); |
| EventCountView* v4 = new EventCountView(); |
| v4->SetBounds(0, 0, 10, 10); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget->GetRootView()); |
| root_view->AddChildViewRaw(v1); |
| v1->AddChildViewRaw(v2); |
| v2->AddChildViewRaw(v3); |
| v3->AddChildViewRaw(v4); |
| |
| widget->Show(); |
| |
| // Change the handle mode of |v3| to indicate that it would like to handle |
| // gesture events. |
| v3->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| |
| // When no gesture handler is set, dispatching a |
| // ui::EventType::kGestureTapDown should bubble up the views hierarchy until |
| // it reaches the first view that will handle it (|v3|) and then sets the |
| // handler to |v3|. |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| ui::GestureEvent tap_down = |
| CreateTestGestureEvent(ui::EventType::kGestureTapDown, 5, 5); |
| widget->OnGestureEvent(&tap_down); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureTapDown)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureTapDown)); |
| EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kGestureTapDown)); |
| EXPECT_EQ(1, v4->GetEventCount(ui::EventType::kGestureTapDown)); |
| EXPECT_EQ(v3, GetGestureHandler(root_view)); |
| EXPECT_TRUE(tap_down.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // A ui::EventType::kGestureTapCancel event should be dispatched to |v3| |
| // directly. |
| ui::GestureEvent tap_cancel = |
| CreateTestGestureEvent(ui::EventType::kGestureTapCancel, 5, 5); |
| widget->OnGestureEvent(&tap_cancel); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kGestureTapCancel)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureTapCancel)); |
| EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kGestureTapCancel)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureTapCancel)); |
| EXPECT_EQ(v3, GetGestureHandler(root_view)); |
| EXPECT_TRUE(tap_cancel.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // Change the handle mode of |v3| to indicate that it would no longer like |
| // to handle events, and change the mode of |v1| to indicate that it would |
| // like to handle events. |
| v3->set_handle_mode(EventCountView::HandleMode::kPropagateEvents); |
| v1->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| |
| // Dispatch a ui::EventType::kGestureScrollBegin event. Because the current |
| // gesture handler (|v3|) does not handle scroll events, the event should |
| // bubble up the views hierarchy until it reaches the first view that will |
| // handle it (|v1|) and then sets the handler to |v1|. |
| ui::GestureEvent scroll_begin = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollBegin, 5, 5); |
| widget->OnGestureEvent(&scroll_begin); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kGestureScrollBegin)); |
| EXPECT_EQ(1, v2->GetEventCount(ui::EventType::kGestureScrollBegin)); |
| EXPECT_EQ(1, v3->GetEventCount(ui::EventType::kGestureScrollBegin)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureScrollBegin)); |
| EXPECT_EQ(v1, GetGestureHandler(root_view)); |
| EXPECT_TRUE(scroll_begin.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // A ui::EventType::kGestureScrollUpdate event should be dispatched to |v1| |
| // directly. |
| ui::GestureEvent scroll_update = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollUpdate, 5, 5); |
| widget->OnGestureEvent(&scroll_update); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureScrollUpdate)); |
| EXPECT_EQ(v1, GetGestureHandler(root_view)); |
| EXPECT_TRUE(scroll_update.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // A ui::EventType::kGestureScrollEnd event should be dispatched to |v1| |
| // directly and should not reset the gesture handler. |
| ui::GestureEvent scroll_end = |
| CreateTestGestureEvent(ui::EventType::kGestureScrollEnd, 5, 5); |
| widget->OnGestureEvent(&scroll_end); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureScrollEnd)); |
| EXPECT_EQ(v1, GetGestureHandler(root_view)); |
| EXPECT_TRUE(scroll_end.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // A ui::EventType::kGesturePinchBegin event (which is a non-scroll event) |
| // should still be dispatched to |v1| directly. |
| ui::GestureEvent pinch_begin = |
| CreateTestGestureEvent(ui::EventType::kGesturePinchBegin, 5, 5); |
| widget->OnGestureEvent(&pinch_begin); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kGesturePinchBegin)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGesturePinchBegin)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGesturePinchBegin)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGesturePinchBegin)); |
| EXPECT_EQ(v1, GetGestureHandler(root_view)); |
| EXPECT_TRUE(pinch_begin.handled()); |
| v1->ResetCounts(); |
| v2->ResetCounts(); |
| v3->ResetCounts(); |
| v4->ResetCounts(); |
| |
| // A ui::EventType::kGestureEnd event should be dispatched to |v1| and should |
| // set the gesture handler to NULL. |
| ui::GestureEvent end = |
| CreateTestGestureEvent(ui::EventType::kGestureEnd, 5, 5); |
| widget->OnGestureEvent(&end); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v3->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(0, v4->GetEventCount(ui::EventType::kGestureEnd)); |
| EXPECT_EQ(nullptr, GetGestureHandler(root_view)); |
| EXPECT_TRUE(end.handled()); |
| |
| widget->Close(); |
| } |
| |
| // TODO(b/271490637): on Mac a drag controller should still be notified when |
| // drag will start. Figure out how to write a unit test for Mac. Then remove |
| // this build flag check. |
| #if !BUILDFLAG(IS_MAC) |
| |
| // Verifies that the drag controller is notified when the view drag will start. |
| TEST_F(WidgetTest, NotifyDragControllerWhenDragWillStart) { |
| // Create a widget whose contents view is draggable. |
| UniqueWidgetPtr widget(std::make_unique<Widget>()); |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP); |
| params.bounds = gfx::Rect(/*width=*/650, /*height=*/650); |
| widget->Init(std::move(params)); |
| widget->Show(); |
| MockDragController mock_drag_controller; |
| views::View contents_view; |
| contents_view.set_drag_controller(&mock_drag_controller); |
| widget->SetContentsView(&contents_view); |
| |
| // Expect the drag controller is notified of the drag start. |
| EXPECT_CALL(mock_drag_controller, OnWillStartDragForView(&contents_view)); |
| |
| // Drag-and-drop `contents_view` by mouse. |
| ui::test::EventGenerator generator(GetContext(), widget->GetNativeWindow()); |
| generator.MoveMouseTo(contents_view.GetBoundsInScreen().CenterPoint()); |
| generator.PressLeftButton(); |
| generator.MoveMouseBy(/*x=*/200, /*y=*/0); |
| generator.ReleaseLeftButton(); |
| } |
| |
| #endif // !BUILDFLAG(IS_MAC) |
| |
| // A class used in WidgetTest.GestureEventLocationWhileBubbling to verify |
| // that when a gesture event bubbles up a View hierarchy, the location |
| // of a gesture event seen by each View is in the local coordinate space |
| // of that View. |
| class GestureLocationView : public EventCountView { |
| METADATA_HEADER(GestureLocationView, EventCountView) |
| |
| public: |
| GestureLocationView() = default; |
| |
| GestureLocationView(const GestureLocationView&) = delete; |
| GestureLocationView& operator=(const GestureLocationView&) = delete; |
| |
| ~GestureLocationView() override = default; |
| |
| void set_expected_location(gfx::Point expected_location) { |
| expected_location_ = expected_location; |
| } |
| |
| // EventCountView: |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| EventCountView::OnGestureEvent(event); |
| |
| // Verify that the location of |event| is in the local coordinate |
| // space of |this|. |
| EXPECT_EQ(expected_location_, event->location()); |
| } |
| |
| private: |
| // The expected location of a gesture event dispatched to |this|. |
| gfx::Point expected_location_; |
| }; |
| |
| BEGIN_METADATA(GestureLocationView) |
| END_METADATA |
| |
| // Verifies that the location of a gesture event is always in the local |
| // coordinate space of the View receiving the event while bubbling. |
| TEST_F(WidgetTest, GestureEventLocationWhileBubbling) { |
| Widget* widget = CreateTopLevelNativeWidget(); |
| widget->SetBounds(gfx::Rect(0, 0, 300, 300)); |
| |
| // Define a hierarchy of three views (coordinates shown below are in the |
| // coordinate space of the root view, but the coordinates used for |
| // SetBounds() are in their parent coordinate space). |
| // v1 (50, 50, 150, 150) |
| // v2 (100, 70, 50, 80) |
| // v3 (120, 100, 10, 10) |
| GestureLocationView* v1 = new GestureLocationView(); |
| v1->SetBounds(50, 50, 150, 150); |
| GestureLocationView* v2 = new GestureLocationView(); |
| v2->SetBounds(50, 20, 50, 80); |
| GestureLocationView* v3 = new GestureLocationView(); |
| v3->SetBounds(20, 30, 10, 10); |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget->GetRootView()); |
| root_view->AddChildViewRaw(v1); |
| v1->AddChildViewRaw(v2); |
| v2->AddChildViewRaw(v3); |
| |
| widget->Show(); |
| |
| // Define a GESTURE_TAP event located at (125, 105) in root view coordinates. |
| // This event is contained within all of |v1|, |v2|, and |v3|. |
| gfx::Point location_in_root(125, 105); |
| ui::GestureEvent tap = CreateTestGestureEvent( |
| ui::EventType::kGestureTap, location_in_root.x(), location_in_root.y()); |
| |
| // Calculate the location of the event in the local coordinate spaces |
| // of each of the views. |
| gfx::Point location_in_v1(ConvertPointFromWidgetToView(v1, location_in_root)); |
| EXPECT_EQ(gfx::Point(75, 55), location_in_v1); |
| gfx::Point location_in_v2(ConvertPointFromWidgetToView(v2, location_in_root)); |
| EXPECT_EQ(gfx::Point(25, 35), location_in_v2); |
| gfx::Point location_in_v3(ConvertPointFromWidgetToView(v3, location_in_root)); |
| EXPECT_EQ(gfx::Point(5, 5), location_in_v3); |
| |
| // Dispatch the event. When each view receives the event, its location should |
| // be in the local coordinate space of that view (see the check made by |
| // GestureLocationView). After dispatch is complete the event's location |
| // should be in the root coordinate space. |
| v1->set_expected_location(location_in_v1); |
| v2->set_expected_location(location_in_v2); |
| v3->set_expected_location(location_in_v3); |
| widget->OnGestureEvent(&tap); |
| EXPECT_EQ(location_in_root, tap.location()); |
| |
| // Verify that each view did in fact see the event. |
| EventCountView* view1 = v1; |
| EventCountView* view2 = v2; |
| EventCountView* view3 = v3; |
| EXPECT_EQ(1, view1->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(1, view2->GetEventCount(ui::EventType::kGestureTap)); |
| EXPECT_EQ(1, view3->GetEventCount(ui::EventType::kGestureTap)); |
| |
| widget->Close(); |
| } |
| |
| // Test the result of Widget::GetAllChildWidgets(). |
| TEST_F(WidgetTest, GetAllChildWidgets) { |
| // Create the following widget hierarchy: |
| // |
| // toplevel |
| // +-- w1 |
| // +-- w11 |
| // +-- w2 |
| // +-- w21 |
| // +-- w22 |
| WidgetAutoclosePtr toplevel(CreateTopLevelPlatformWidget()); |
| Widget* w1 = CreateChildPlatformWidget(toplevel->GetNativeView()); |
| Widget* w11 = CreateChildPlatformWidget(w1->GetNativeView()); |
| Widget* w2 = CreateChildPlatformWidget(toplevel->GetNativeView()); |
| Widget* w21 = CreateChildPlatformWidget(w2->GetNativeView()); |
| Widget* w22 = CreateChildPlatformWidget(w2->GetNativeView()); |
| |
| std::set<Widget*> expected; |
| expected.insert(toplevel.get()); |
| expected.insert(w1); |
| expected.insert(w11); |
| expected.insert(w2); |
| expected.insert(w21); |
| expected.insert(w22); |
| |
| Widget::Widgets child_widgets = |
| Widget::GetAllChildWidgets(toplevel->GetNativeView()); |
| |
| EXPECT_TRUE(std::ranges::equal(expected, child_widgets)); |
| |
| // Check GetAllOwnedWidgets(). On Aura, this includes "transient" children. |
| // Otherwise (on all platforms), it should be the same as GetAllChildWidgets() |
| // except the root Widget is not included. |
| EXPECT_EQ(1u, expected.erase(toplevel.get())); |
| |
| Widget::Widgets owned_widgets = |
| Widget::GetAllOwnedWidgets(toplevel->GetNativeView()); |
| |
| EXPECT_TRUE(std::ranges::equal(expected, owned_widgets)); |
| } |
| |
| // Test the result of Widget::ForEachOwnedWidget(). |
| TEST_F(WidgetTest, ForEachOwnedWidget) { |
| // Create the following widget hierarchy: |
| // |
| // toplevel |
| // +-- w1 |
| // +-- w11 |
| // +-- w2 |
| // +-- w21 |
| // +-- w22 |
| WidgetAutoclosePtr toplevel(CreateTopLevelPlatformWidget()); |
| Widget* w1 = CreateChildPlatformWidget(toplevel->GetNativeView()); |
| Widget* w11 = CreateChildPlatformWidget(w1->GetNativeView()); |
| Widget* w2 = CreateChildPlatformWidget(toplevel->GetNativeView()); |
| Widget* w21 = CreateChildPlatformWidget(w2->GetNativeView()); |
| Widget* w22 = CreateChildPlatformWidget(w2->GetNativeView()); |
| |
| std::set<Widget*> expected; |
| expected.insert(w1); |
| expected.insert(w11); |
| expected.insert(w2); |
| expected.insert(w21); |
| expected.insert(w22); |
| |
| Widget::Widgets widgets; |
| Widget::ForEachOwnedWidget( |
| toplevel->GetNativeView(), |
| [&widgets](Widget* widget) { widgets.insert(widget); }); |
| |
| EXPECT_TRUE(std::ranges::equal(expected, widgets)); |
| } |
| |
| // Test that ForEachOwnedWidget is robust to deletion. |
| TEST_F(WidgetTest, ForEachOwnedWidget_WithDeletion) { |
| // Create the following widget hierarchy: |
| // |
| // toplevel |
| // +-- w1 |
| // +-- w2 |
| // +-- w3 |
| WidgetAutoclosePtr toplevel(CreateTopLevelPlatformWidget()); |
| Widget* w1 = CreateChildPlatformWidget(toplevel->GetNativeView()); |
| Widget* w2 = CreateChildPlatformWidget(toplevel->GetNativeView()); |
| Widget* w3 = CreateChildPlatformWidget(toplevel->GetNativeView()); |
| |
| // We need to delete a widget from another widget's callback. |
| // The iteration order is pointer-based, so we sort them to find |
| // two widgets where we can guarantee the order. |
| std::vector<Widget*> children = {w1, w2, w3}; |
| std::sort(children.begin(), children.end()); |
| |
| Widget* first_widget = children[0]; |
| Widget* second_widget = children[1]; |
| |
| std::set<Widget*> visited_widgets; |
| Widget::ForEachOwnedWidget(toplevel->GetNativeView(), [&](Widget* widget) { |
| if (widget == first_widget) { |
| second_widget->CloseNow(); |
| } |
| visited_widgets.insert(widget); |
| }); |
| |
| EXPECT_EQ(2u, visited_widgets.size()); |
| EXPECT_TRUE(visited_widgets.count(first_widget)); |
| EXPECT_FALSE(visited_widgets.count(second_widget)); |
| EXPECT_TRUE(visited_widgets.count(children[2])); |
| } |
| |
| // Used by DestroyChildWidgetsInOrder. On destruction adds the supplied name to |
| // a vector. |
| class DestroyedTrackingView : public View { |
| METADATA_HEADER(DestroyedTrackingView, View) |
| |
| public: |
| DestroyedTrackingView(const std::string& name, |
| std::vector<std::string>* add_to) |
| : name_(name), add_to_(add_to) {} |
| |
| DestroyedTrackingView(const DestroyedTrackingView&) = delete; |
| DestroyedTrackingView& operator=(const DestroyedTrackingView&) = delete; |
| |
| ~DestroyedTrackingView() override { add_to_->push_back(name_); } |
| |
| private: |
| const std::string name_; |
| raw_ptr<std::vector<std::string>> add_to_; |
| }; |
| |
| BEGIN_METADATA(DestroyedTrackingView) |
| END_METADATA |
| |
| class WidgetChildDestructionTest : public DesktopWidgetTest { |
| public: |
| WidgetChildDestructionTest() = default; |
| |
| WidgetChildDestructionTest(const WidgetChildDestructionTest&) = delete; |
| WidgetChildDestructionTest& operator=(const WidgetChildDestructionTest&) = |
| delete; |
| |
| // Creates a top level and a child, destroys the child and verifies the views |
| // of the child are destroyed before the views of the parent. |
| void RunDestroyChildWidgetsTest(bool top_level_has_desktop_native_widget_aura, |
| bool child_has_desktop_native_widget_aura) { |
| // When a View is destroyed its name is added here. |
| std::vector<std::string> destroyed; |
| |
| Widget* top_level = new Widget; |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); |
| if (!top_level_has_desktop_native_widget_aura) { |
| params.native_widget = |
| CreatePlatformNativeWidgetImpl(top_level, kStubCapture, nullptr); |
| } |
| top_level->Init(std::move(params)); |
| top_level->GetRootView()->AddChildViewRaw( |
| new DestroyedTrackingView("parent", &destroyed)); |
| top_level->Show(); |
| |
| Widget* child = new Widget; |
| Widget::InitParams child_params = |
| CreateParams(Widget::InitParams::TYPE_POPUP); |
| child_params.parent = top_level->GetNativeView(); |
| if (!child_has_desktop_native_widget_aura) { |
| child_params.native_widget = |
| CreatePlatformNativeWidgetImpl(child, kStubCapture, nullptr); |
| } |
| child->Init(std::move(child_params)); |
| child->GetRootView()->AddChildViewRaw( |
| new DestroyedTrackingView("child", &destroyed)); |
| child->Show(); |
| |
| // Should trigger destruction of the child too. |
| top_level->native_widget_private()->CloseNow(); |
| |
| // Child should be destroyed first. |
| ASSERT_EQ(2u, destroyed.size()); |
| EXPECT_EQ("child", destroyed[0]); |
| EXPECT_EQ("parent", destroyed[1]); |
| } |
| }; |
| |
| // See description of RunDestroyChildWidgetsTest(). Parent uses |
| // DesktopNativeWidgetAura. |
| TEST_F(WidgetChildDestructionTest, |
| DestroyChildWidgetsInOrderWithDesktopNativeWidget) { |
| RunDestroyChildWidgetsTest(true, false); |
| } |
| |
| // See description of RunDestroyChildWidgetsTest(). Both parent and child use |
| // DesktopNativeWidgetAura. |
| TEST_F(WidgetChildDestructionTest, |
| DestroyChildWidgetsInOrderWithDesktopNativeWidgetForBoth) { |
| RunDestroyChildWidgetsTest(true, true); |
| } |
| |
| // See description of RunDestroyChildWidgetsTest(). |
| TEST_F(WidgetChildDestructionTest, DestroyChildWidgetsInOrder) { |
| RunDestroyChildWidgetsTest(false, false); |
| } |
| |
| // Verifies nativeview visbility matches that of Widget visibility when |
| // SetFullscreen is invoked. |
| TEST_F(WidgetTest, FullscreenStatePropagated) { |
| std::unique_ptr<Widget> top_level_widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| top_level_widget->SetFullscreen(true); |
| EXPECT_EQ(top_level_widget->IsVisible(), |
| IsNativeWindowVisible(top_level_widget->GetNativeWindow())); |
| } |
| |
| // Verifies nativeview visbility matches that of Widget visibility when |
| // SetFullscreen is invoked, for a widget provided with a desktop widget. |
| TEST_F(DesktopWidgetTest, FullscreenStatePropagated_DesktopWidget) { |
| std::unique_ptr<Widget> top_level_widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| top_level_widget->SetFullscreen(true); |
| EXPECT_EQ(top_level_widget->IsVisible(), |
| IsNativeWindowVisible(top_level_widget->GetNativeWindow())); |
| } |
| |
| // Used to delete the widget when the supplied bounds changes. |
| class DestroyingWidgetBoundsObserver : public WidgetObserver { |
| public: |
| explicit DestroyingWidgetBoundsObserver(Widget* widget) { |
| widget_observation_.Observe(widget); |
| } |
| |
| // There are no assertions here as not all platforms call |
| // OnWidgetBoundsChanged() when going fullscreen. |
| ~DestroyingWidgetBoundsObserver() override = default; |
| |
| // WidgetObserver: |
| void OnWidgetBoundsChanged(Widget* widget, |
| const gfx::Rect& new_bounds) override { |
| widget_observation_.Reset(); |
| } |
| |
| private: |
| base::ScopedObservation<Widget, WidgetObserver> widget_observation_{this}; |
| }; |
| |
| // Deletes a Widget when the bounds change as part of toggling fullscreen. |
| // This is a regression test for https://crbug.com/1197436. |
| // Disabled on Mac: This test has historically deleted the Widget not during |
| // SetFullscreen, but at the end of the test. When the Widget is deleted inside |
| // SetFullscreen, the test crashes. |
| // https://crbug.com/1307486 |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_DeleteInSetFullscreen DISABLED_DeleteInSetFullscreen |
| #else |
| #define MAYBE_DeleteInSetFullscreen DeleteInSetFullscreen |
| #endif |
| TEST_F(DesktopWidgetTest, MAYBE_DeleteInSetFullscreen) { |
| std::unique_ptr<Widget> widget = std::make_unique<Widget>(); |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); |
| params.ownership = Widget::InitParams::CLIENT_OWNS_WIDGET; |
| widget->Init(std::move(params)); |
| Widget* w = widget.get(); |
| DestroyingWidgetBoundsObserver destroyer(w); |
| w->SetFullscreen(true); |
| } |
| |
| // Tests that frame Layout is called when a widget goes fullscreen without |
| // changing its size or title. |
| TEST_F(WidgetTest, FullscreenFrameLayout) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| |
| auto frame_view = std::make_unique<ConfigurableTestFrameView>(widget.get()); |
| ConfigurableTestFrameView* frame = frame_view.get(); |
| widget->non_client_view()->SetFrameView(std::move(frame_view)); |
| |
| widget->Maximize(); |
| RunPendingMessages(); |
| |
| EXPECT_FALSE(frame->fullscreen_layout_called()); |
| widget->SetFullscreen(true); |
| widget->Show(); |
| #if BUILDFLAG(IS_MAC) |
| // On macOS, a fullscreen layout is triggered from within SetFullscreen. |
| // https://crbug.com/1307496 |
| EXPECT_TRUE(frame->fullscreen_layout_called()); |
| #else |
| EXPECT_TRUE(ViewTestApi(frame).needs_layout()); |
| #endif |
| widget->LayoutRootViewIfNecessary(); |
| RunPendingMessages(); |
| |
| EXPECT_TRUE(frame->fullscreen_layout_called()); |
| } |
| |
| namespace { |
| |
| // Trivial WidgetObserverTest that invokes Widget::IsActive() from |
| // OnWindowDestroying. |
| class IsActiveFromDestroyObserver : public WidgetObserver { |
| public: |
| explicit IsActiveFromDestroyObserver(Widget* widget) { |
| widget_observation_.Observe(widget); |
| } |
| |
| IsActiveFromDestroyObserver(const IsActiveFromDestroyObserver&) = delete; |
| IsActiveFromDestroyObserver& operator=(const IsActiveFromDestroyObserver&) = |
| delete; |
| |
| ~IsActiveFromDestroyObserver() override = default; |
| void OnWidgetDestroying(Widget* widget) override { |
| widget->IsActive(); |
| widget_observation_.Reset(); |
| } |
| |
| private: |
| base::ScopedObservation<Widget, WidgetObserver> widget_observation_{this}; |
| }; |
| |
| } // namespace |
| |
| class ChildDesktopWidgetTest : public DesktopWidgetTest { |
| public: |
| Widget::InitParams CreateParams(Widget::InitParams::Ownership ownership, |
| Widget::InitParams::Type type) override { |
| Widget::InitParams params = |
| DesktopWidgetTest::CreateParams(ownership, type); |
| if (context_) { |
| params.context = context_; |
| } |
| return params; |
| } |
| |
| std::unique_ptr<Widget> CreateChildWidget(gfx::NativeWindow context) { |
| context_ = context; |
| return CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| } |
| |
| private: |
| gfx::NativeWindow context_ = gfx::NativeWindow(); |
| }; |
| |
| // Verifies Widget::IsActive() invoked from |
| // WidgetObserver::OnWidgetDestroying() in a child widget doesn't crash. |
| TEST_F(ChildDesktopWidgetTest, IsActiveFromDestroy) { |
| // Create two widgets, one a child of the other. |
| std::unique_ptr<Widget> parent_widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| parent_widget->Show(); |
| |
| std::unique_ptr<Widget> child_widget = |
| CreateChildWidget(parent_widget->GetNativeWindow()); |
| IsActiveFromDestroyObserver observer(child_widget.get()); |
| child_widget->Show(); |
| |
| parent_widget->CloseNow(); |
| } |
| |
| // Tests that events propagate through from the dispatcher with the correct |
| // event type, and that the different platforms behave the same. |
| TEST_F(WidgetTest, MouseEventTypesViaGenerator) { |
| WidgetAutoclosePtr widget(CreateTopLevelFramelessPlatformWidget()); |
| EventCountView* view = |
| widget->GetRootView()->AddChildView(std::make_unique<EventCountView>()); |
| view->set_handle_mode(EventCountView::HandleMode::kConsumeEvents); |
| view->SetBounds(10, 10, 50, 40); |
| |
| widget->SetBounds(gfx::Rect(0, 0, 100, 80)); |
| widget->Show(); |
| |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| const gfx::Point view_center_point = view->GetBoundsInScreen().CenterPoint(); |
| generator->set_current_screen_location(view_center_point); |
| |
| generator->ClickLeftButton(); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMousePressed)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseReleased)); |
| EXPECT_EQ(ui::EF_LEFT_MOUSE_BUTTON, view->last_flags()); |
| |
| generator->PressRightButton(); |
| EXPECT_EQ(2, view->GetEventCount(ui::EventType::kMousePressed)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseReleased)); |
| EXPECT_EQ(ui::EF_RIGHT_MOUSE_BUTTON, view->last_flags()); |
| |
| generator->ReleaseRightButton(); |
| EXPECT_EQ(2, view->GetEventCount(ui::EventType::kMousePressed)); |
| EXPECT_EQ(2, view->GetEventCount(ui::EventType::kMouseReleased)); |
| EXPECT_EQ(ui::EF_RIGHT_MOUSE_BUTTON, view->last_flags()); |
| |
| // Test mouse move events. |
| EXPECT_EQ(0, view->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(0, view->GetEventCount(ui::EventType::kMouseEntered)); |
| |
| // Move the mouse a displacement of (10, 10). |
| generator->MoveMouseTo(view_center_point + gfx::Vector2d(10, 10)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(ui::EF_NONE, view->last_flags()); |
| |
| // Move it again - entered count shouldn't change. |
| generator->MoveMouseTo(view_center_point + gfx::Vector2d(11, 11)); |
| EXPECT_EQ(2, view->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(0, view->GetEventCount(ui::EventType::kMouseExited)); |
| |
| // Move it off the view. |
| const gfx::Point out_of_bounds_point = |
| view->GetBoundsInScreen().bottom_right() + gfx::Vector2d(10, 10); |
| generator->MoveMouseTo(out_of_bounds_point); |
| EXPECT_EQ(2, view->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseExited)); |
| |
| // Move it back on. |
| generator->MoveMouseTo(view_center_point); |
| EXPECT_EQ(3, view->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(2, view->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseExited)); |
| |
| // Drargging. Cover HasCapture() and NativeWidgetPrivate::IsMouseButtonDown(). |
| generator->DragMouseTo(out_of_bounds_point); |
| EXPECT_EQ(3, view->GetEventCount(ui::EventType::kMousePressed)); |
| EXPECT_EQ(3, view->GetEventCount(ui::EventType::kMouseReleased)); |
| EXPECT_EQ(1, view->GetEventCount(ui::EventType::kMouseDragged)); |
| EXPECT_EQ(ui::EF_LEFT_MOUSE_BUTTON, view->last_flags()); |
| } |
| |
| // Tests that the root view is correctly set up for Widget types that do not |
| // require a non-client view, before any other views are added to the widget. |
| // That is, before Widget::ReorderNativeViews() is called which, if called with |
| // a root view not set, could cause the root view to get resized to the widget. |
| TEST_F(WidgetTest, NonClientWindowValidAfterInit) { |
| WidgetAutoclosePtr widget(CreateTopLevelFramelessPlatformWidget()); |
| View* root_view = widget->GetRootView(); |
| |
| // Size the root view to exceed the widget bounds. |
| const gfx::Rect test_rect(0, 0, 500, 500); |
| root_view->SetBoundsRect(test_rect); |
| |
| EXPECT_NE(test_rect.size(), widget->GetWindowBoundsInScreen().size()); |
| |
| EXPECT_EQ(test_rect, root_view->bounds()); |
| widget->ReorderNativeViews(); |
| EXPECT_EQ(test_rect, root_view->bounds()); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| // Provides functionality to subclass a window and keep track of messages |
| // received. |
| class SubclassWindowHelper { |
| public: |
| explicit SubclassWindowHelper(HWND window) |
| : window_(window), message_to_destroy_on_(0) { |
| EXPECT_EQ(instance_, nullptr); |
| instance_ = this; |
| EXPECT_TRUE(Subclass()); |
| } |
| |
| SubclassWindowHelper(const SubclassWindowHelper&) = delete; |
| SubclassWindowHelper& operator=(const SubclassWindowHelper&) = delete; |
| |
| ~SubclassWindowHelper() { |
| Unsubclass(); |
| instance_ = nullptr; |
| } |
| |
| // Returns true if the |message| passed in was received. |
| bool received_message(unsigned int message) { |
| return (messages_.find(message) != messages_.end()); |
| } |
| |
| void Clear() { messages_.clear(); } |
| |
| void set_message_to_destroy_on(unsigned int message) { |
| message_to_destroy_on_ = message; |
| } |
| |
| private: |
| bool Subclass() { |
| old_proc_ = reinterpret_cast<WNDPROC>(::SetWindowLongPtr( |
| window_, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WndProc))); |
| return old_proc_ != nullptr; |
| } |
| |
| void Unsubclass() { |
| ::SetWindowLongPtr(window_, GWLP_WNDPROC, |
| reinterpret_cast<LONG_PTR>(old_proc_)); |
| } |
| |
| static LRESULT CALLBACK WndProc(HWND window, |
| unsigned int message, |
| WPARAM w_param, |
| LPARAM l_param) { |
| EXPECT_NE(instance_, nullptr); |
| EXPECT_EQ(window, instance_->window_); |
| |
| // Keep track of messags received for this window. |
| instance_->messages_.insert(message); |
| |
| LRESULT ret = ::CallWindowProc(instance_->old_proc_, window, message, |
| w_param, l_param); |
| if (message == instance_->message_to_destroy_on_) { |
| instance_->Unsubclass(); |
| ::DestroyWindow(window); |
| } |
| return ret; |
| } |
| |
| WNDPROC old_proc_; |
| HWND window_; |
| static SubclassWindowHelper* instance_; |
| std::set<unsigned int> messages_; |
| unsigned int message_to_destroy_on_; |
| }; |
| |
| SubclassWindowHelper* SubclassWindowHelper::instance_ = nullptr; |
| |
| // This test validates whether the WM_SYSCOMMAND message for SC_MOVE is |
| // received when we post a WM_NCLBUTTONDOWN message for the caption in the |
| // following scenarios:- |
| // 1. Posting a WM_NCMOUSEMOVE message for a different location. |
| // 2. Posting a WM_NCMOUSEMOVE message with a different hittest code. |
| // 3. Posting a WM_MOUSEMOVE message. |
| // Disabled because of flaky timeouts: http://crbug.com/592742 |
| TEST_F(DesktopWidgetTest, |
| DISABLED_SysCommandMoveOnNCLButtonDownOnCaptionAndMoveTest) { |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| widget->Show(); |
| ::SetCursorPos(500, 500); |
| |
| HWND window = widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); |
| |
| SubclassWindowHelper subclass_helper(window); |
| |
| // Posting just a WM_NCLBUTTONDOWN message should not result in a |
| // WM_SYSCOMMAND |
| ::PostMessage(window, WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(100, 100)); |
| RunPendingMessages(); |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCLBUTTONDOWN)); |
| EXPECT_FALSE(subclass_helper.received_message(WM_SYSCOMMAND)); |
| |
| subclass_helper.Clear(); |
| // Posting a WM_NCLBUTTONDOWN message followed by a WM_NCMOUSEMOVE at the |
| // same location should not result in a WM_SYSCOMMAND message. |
| ::PostMessage(window, WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(100, 100)); |
| ::PostMessage(window, WM_NCMOUSEMOVE, HTCAPTION, MAKELPARAM(100, 100)); |
| RunPendingMessages(); |
| |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCLBUTTONDOWN)); |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCMOUSEMOVE)); |
| EXPECT_FALSE(subclass_helper.received_message(WM_SYSCOMMAND)); |
| |
| subclass_helper.Clear(); |
| // Posting a WM_NCLBUTTONDOWN message followed by a WM_NCMOUSEMOVE at a |
| // different location should result in a WM_SYSCOMMAND message. |
| ::PostMessage(window, WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(100, 100)); |
| ::PostMessage(window, WM_NCMOUSEMOVE, HTCAPTION, MAKELPARAM(110, 110)); |
| RunPendingMessages(); |
| |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCLBUTTONDOWN)); |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCMOUSEMOVE)); |
| EXPECT_TRUE(subclass_helper.received_message(WM_SYSCOMMAND)); |
| |
| subclass_helper.Clear(); |
| // Posting a WM_NCLBUTTONDOWN message followed by a WM_NCMOUSEMOVE at a |
| // different location with a different hittest code should result in a |
| // WM_SYSCOMMAND message. |
| ::PostMessage(window, WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(100, 100)); |
| ::PostMessage(window, WM_NCMOUSEMOVE, HTTOP, MAKELPARAM(110, 102)); |
| RunPendingMessages(); |
| |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCLBUTTONDOWN)); |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCMOUSEMOVE)); |
| EXPECT_TRUE(subclass_helper.received_message(WM_SYSCOMMAND)); |
| |
| subclass_helper.Clear(); |
| // Posting a WM_NCLBUTTONDOWN message followed by a WM_MOUSEMOVE should |
| // result in a WM_SYSCOMMAND message. |
| ::PostMessage(window, WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(100, 100)); |
| ::PostMessage(window, WM_MOUSEMOVE, HTCLIENT, MAKELPARAM(110, 110)); |
| RunPendingMessages(); |
| |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCLBUTTONDOWN)); |
| EXPECT_TRUE(subclass_helper.received_message(WM_MOUSEMOVE)); |
| EXPECT_TRUE(subclass_helper.received_message(WM_SYSCOMMAND)); |
| } |
| |
| // This test validates that destroying the window in the context of the |
| // WM_SYSCOMMAND message with SC_MOVE does not crash. |
| // Disabled because of flaky timeouts: http://crbug.com/592742 |
| TEST_F(DesktopWidgetTest, DISABLED_DestroyInSysCommandNCLButtonDownOnCaption) { |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| widget->Show(); |
| ::SetCursorPos(500, 500); |
| |
| HWND window = widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); |
| |
| SubclassWindowHelper subclass_helper(window); |
| |
| // Destroying the window in the context of the WM_SYSCOMMAND message |
| // should not crash. |
| subclass_helper.set_message_to_destroy_on(WM_SYSCOMMAND); |
| |
| ::PostMessage(window, WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(100, 100)); |
| ::PostMessage(window, WM_NCMOUSEMOVE, HTCAPTION, MAKELPARAM(110, 110)); |
| RunPendingMessages(); |
| |
| EXPECT_TRUE(subclass_helper.received_message(WM_NCLBUTTONDOWN)); |
| EXPECT_TRUE(subclass_helper.received_message(WM_SYSCOMMAND)); |
| } |
| |
| #endif |
| |
| // Test that the z-order levels round-trip. |
| TEST_F(WidgetTest, ZOrderLevel) { |
| WidgetAutoclosePtr widget(CreateTopLevelNativeWidget()); |
| EXPECT_EQ(ui::ZOrderLevel::kNormal, widget->GetZOrderLevel()); |
| widget->SetZOrderLevel(ui::ZOrderLevel::kFloatingWindow); |
| EXPECT_EQ(ui::ZOrderLevel::kFloatingWindow, widget->GetZOrderLevel()); |
| widget->SetZOrderLevel(ui::ZOrderLevel::kNormal); |
| EXPECT_EQ(ui::ZOrderLevel::kNormal, widget->GetZOrderLevel()); |
| } |
| |
| namespace { |
| |
| class ScaleFactorView : public View { |
| METADATA_HEADER(ScaleFactorView, View) |
| |
| public: |
| ScaleFactorView() = default; |
| |
| ScaleFactorView(const ScaleFactorView&) = delete; |
| ScaleFactorView& operator=(const ScaleFactorView&) = delete; |
| |
| // Overridden from ui::LayerDelegate: |
| void OnDeviceScaleFactorChanged(float old_device_scale_factor, |
| float new_device_scale_factor) override { |
| last_scale_factor_ = new_device_scale_factor; |
| View::OnDeviceScaleFactorChanged(old_device_scale_factor, |
| new_device_scale_factor); |
| } |
| |
| float last_scale_factor() const { return last_scale_factor_; } |
| |
| private: |
| float last_scale_factor_ = 0.f; |
| }; |
| |
| BEGIN_METADATA(ScaleFactorView) |
| END_METADATA |
| |
| } // namespace |
| |
| // Ensure scale factor changes are propagated from the native Widget. |
| TEST_F(WidgetTest, OnDeviceScaleFactorChanged) { |
| // Automatically close the widget, but not delete it. |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| ScaleFactorView* view = new ScaleFactorView; |
| widget->GetRootView()->AddChildViewRaw(view); |
| float scale_factor = widget->GetLayer()->device_scale_factor(); |
| EXPECT_NE(scale_factor, 0.f); |
| |
| // For views that are not layer-backed, adding the view won't notify the view |
| // about the initial scale factor. Fake it. |
| view->OnDeviceScaleFactorChanged(0.f, scale_factor); |
| EXPECT_EQ(scale_factor, view->last_scale_factor()); |
| |
| // Changes should be propagated. |
| scale_factor *= 2.0f; |
| widget->GetLayer()->OnDeviceScaleFactorChanged(scale_factor); |
| EXPECT_EQ(scale_factor, view->last_scale_factor()); |
| } |
| |
| // Test that WidgetRemovalsObserver::OnWillRemoveView is called when deleting |
| // a view. |
| TEST_F(WidgetTest, WidgetRemovalsObserverCalled) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| TestWidgetRemovalsObserver removals_observer; |
| widget->AddRemovalsObserver(&removals_observer); |
| |
| View* parent = new View(); |
| widget->client_view()->AddChildViewRaw(parent); |
| |
| View* child = new View(); |
| parent->AddChildViewRaw(child); |
| |
| widget->client_view()->RemoveChildView(parent); |
| EXPECT_TRUE(removals_observer.DidRemoveView(parent)); |
| EXPECT_FALSE(removals_observer.DidRemoveView(child)); |
| |
| // Calling RemoveChildView() doesn't delete the view, but deleting |
| // |parent| will automatically delete |child|. |
| delete parent; |
| |
| widget->RemoveRemovalsObserver(&removals_observer); |
| } |
| |
| // Test that WidgetRemovalsObserver::OnWillRemoveView is called when deleting |
| // the root view. |
| TEST_F(WidgetTest, WidgetRemovalsObserverCalledWhenRemovingRootView) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| TestWidgetRemovalsObserver removals_observer; |
| widget->AddRemovalsObserver(&removals_observer); |
| views::View* root_view = widget->GetRootView(); |
| |
| widget.reset(); |
| EXPECT_TRUE(removals_observer.DidRemoveView(root_view)); |
| } |
| |
| // Test that WidgetRemovalsObserver::OnWillRemoveView is called when moving |
| // a view from one widget to another, but not when moving a view within |
| // the same widget. |
| TEST_F(WidgetTest, WidgetRemovalsObserverCalledWhenMovingBetweenWidgets) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| TestWidgetRemovalsObserver removals_observer; |
| widget->AddRemovalsObserver(&removals_observer); |
| |
| View* parent = new View(); |
| widget->client_view()->AddChildViewRaw(parent); |
| |
| View* child = new View(); |
| widget->client_view()->AddChildViewRaw(child); |
| |
| // Reparenting the child shouldn't call the removals observer. |
| parent->AddChildViewRaw(child); |
| EXPECT_FALSE(removals_observer.DidRemoveView(child)); |
| |
| // Moving the child to a different widget should call the removals observer. |
| WidgetAutoclosePtr widget2(CreateTopLevelPlatformWidget()); |
| widget2->client_view()->AddChildViewRaw(child); |
| EXPECT_TRUE(removals_observer.DidRemoveView(child)); |
| |
| widget->RemoveRemovalsObserver(&removals_observer); |
| } |
| |
| // Test dispatch of ui::EventType::kMousewheel. |
| TEST_F(WidgetTest, MouseWheelEvent) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->SetBounds(gfx::Rect(0, 0, 600, 600)); |
| EventCountView* event_count_view = |
| widget->client_view()->AddChildView(std::make_unique<EventCountView>()); |
| event_count_view->SetBounds(0, 0, 600, 600); |
| widget->Show(); |
| |
| auto event_generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| |
| event_generator->MoveMouseWheel(1, 1); |
| EXPECT_EQ(1, event_count_view->GetEventCount(ui::EventType::kMousewheel)); |
| } |
| |
| // Test that ui::EventType::kMouseEntered is dispatched even when not followed |
| // by ui::EventType::kMouseMoved. |
| TEST_F(WidgetTest, MouseEnteredWithoutMoved) { |
| WidgetAutoclosePtr widget(CreateTopLevelFramelessPlatformWidget()); |
| widget->SetBounds(gfx::Rect(0, 0, 100, 100)); |
| |
| auto* root_view = static_cast<internal::RootView*>(widget->GetRootView()); |
| auto* v1 = root_view->AddChildView(std::make_unique<EventCountView>()); |
| v1->SetBounds(10, 10, 10, 10); |
| auto* v2 = root_view->AddChildView(std::make_unique<EventCountView>()); |
| v2->SetBounds(20, 10, 10, 10); |
| |
| widget->Show(); |
| |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| |
| // Enter |v1| and check that it received ui::EventType::kMouseEntered. |
| auto enter_location = v1->GetBoundsInScreen().CenterPoint(); |
| generator->set_current_screen_location(enter_location); |
| generator->SendMouseEnter(); |
| EXPECT_EQ(v1, GetMouseMoveHandler(root_view)); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kMouseExited)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseExited)); |
| |
| // Enter |v2| and check that |v1| received ui::EventType::kMouseExited and |
| // |v2| received ui::EventType::kMouseEntered. |
| enter_location = v2->GetBoundsInScreen().CenterPoint(); |
| generator->set_current_screen_location(enter_location); |
| generator->SendMouseEnter(); |
| EXPECT_EQ(v2, GetMouseMoveHandler(root_view)); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kMouseExited)); |
| EXPECT_EQ(1, v2->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseExited)); |
| |
| // Enter |root_view| and check that |v2| received ui::EventType::kMouseExited. |
| enter_location = gfx::Point(0, 0); |
| generator->set_current_screen_location(enter_location); |
| generator->SendMouseEnter(); |
| EXPECT_EQ(nullptr, GetMouseMoveHandler(root_view)); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(0, v1->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(1, v1->GetEventCount(ui::EventType::kMouseExited)); |
| EXPECT_EQ(1, v2->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(0, v2->GetEventCount(ui::EventType::kMouseMoved)); |
| EXPECT_EQ(1, v2->GetEventCount(ui::EventType::kMouseExited)); |
| } |
| |
| // Test that ui::EventType::kMouseMoved after ui::EventType::kMouseEntered |
| // doesn't cause an extra ui::EventType::kMouseEntered. |
| TEST_F(WidgetTest, MouseMovedAfterEnteredDoesntCauseEntered) { |
| WidgetAutoclosePtr widget(CreateTopLevelFramelessPlatformWidget()); |
| widget->SetBounds(gfx::Rect(0, 0, 100, 100)); |
| |
| auto* root_view = widget->GetRootView(); |
| auto* v = root_view->AddChildView(std::make_unique<EventCountView>()); |
| v->SetBounds(10, 10, 10, 10); |
| |
| widget->Show(); |
| |
| auto generator = |
| CreateEventGenerator(GetContext(), widget->GetNativeWindow()); |
| |
| // Enter |v|. |
| auto enter_location = v->GetBoundsInScreen().CenterPoint(); |
| generator->set_current_screen_location(enter_location); |
| generator->SendMouseEnter(); |
| |
| // Send ui::EventType::kMouseMoved at the same location and check that it |
| // didn't generate ui::EventType::kMouseEntered again. |
| generator->MoveMouseBy(0, 0); |
| EXPECT_EQ(1, v->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(1, v->GetEventCount(ui::EventType::kMouseMoved)); |
| |
| // Reset state by entering |root_view|. |
| generator->MoveMouseTo(0, 0); |
| |
| // Enter |v| again. |
| generator->set_current_screen_location(enter_location); |
| generator->SendMouseEnter(); |
| |
| // Send ui::EventType::kMouseMoved at a slightly offset location and check |
| // that it didn't generate ui::EventType::kMouseEntered again. |
| generator->MoveMouseBy(1, 1); |
| EXPECT_EQ(2, v->GetEventCount(ui::EventType::kMouseEntered)); |
| EXPECT_EQ(2, v->GetEventCount(ui::EventType::kMouseMoved)); |
| } |
| |
| class CloseFromClosingObserver : public WidgetObserver { |
| public: |
| ~CloseFromClosingObserver() override { |
| EXPECT_TRUE(was_on_widget_closing_called_); |
| } |
| // WidgetObserver: |
| void OnWidgetClosing(Widget* widget) override { |
| // OnWidgetClosing() should only be called once, even if Close() is called |
| // after CloseNow(). |
| ASSERT_FALSE(was_on_widget_closing_called_); |
| was_on_widget_closing_called_ = true; |
| widget->Close(); |
| } |
| |
| private: |
| bool was_on_widget_closing_called_ = false; |
| }; |
| |
| TEST_F(WidgetTest, CloseNowFollowedByCloseDoesntCallOnWidgetClosingTwice) { |
| CloseFromClosingObserver observer; |
| std::unique_ptr<Widget> widget = std::make_unique<Widget>(); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| widget->Init(std::move(params)); |
| widget->AddObserver(&observer); |
| widget->CloseNow(); |
| widget->RemoveObserver(&observer); |
| widget.reset(); |
| // Assertions are in CloseFromClosingObserver. |
| } |
| |
| namespace { |
| |
| class TestSaveWindowPlacementWidgetDelegate : public TestDesktopWidgetDelegate { |
| public: |
| TestSaveWindowPlacementWidgetDelegate() = default; |
| TestSaveWindowPlacementWidgetDelegate( |
| const TestSaveWindowPlacementWidgetDelegate&) = delete; |
| TestSaveWindowPlacementWidgetDelegate operator=( |
| const TestSaveWindowPlacementWidgetDelegate&) = delete; |
| ~TestSaveWindowPlacementWidgetDelegate() override = default; |
| |
| void set_should_save_window_placement(bool should_save) { |
| should_save_window_placement_ = should_save; |
| } |
| |
| int save_window_placement_count() const { |
| return save_window_placement_count_; |
| } |
| |
| // ViewsDelegate: |
| std::string GetWindowName() const final { return GetWidget()->GetName(); } |
| bool ShouldSaveWindowPlacement() const final { |
| return should_save_window_placement_; |
| } |
| void SaveWindowPlacement(const gfx::Rect& bounds, |
| ui::mojom::WindowShowState show_state) override { |
| save_window_placement_count_++; |
| } |
| |
| private: |
| bool should_save_window_placement_ = true; |
| int save_window_placement_count_ = 0; |
| }; |
| |
| } // namespace |
| |
| TEST_F(WidgetTest, ShouldSaveWindowPlacement) { |
| for (bool save : {false, true}) { |
| SCOPED_TRACE(save ? "ShouldSave" : "ShouldNotSave"); |
| TestSaveWindowPlacementWidgetDelegate widget_delegate; |
| widget_delegate.set_should_save_window_placement(save); |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); |
| params.ownership = Widget::InitParams::CLIENT_OWNS_WIDGET; |
| params.name = "TestWidget"; |
| widget_delegate.InitWidget(std::move(params)); |
| auto* widget = widget_delegate.GetWidget(); |
| widget->Close(); |
| EXPECT_EQ(save ? 1 : 0, widget_delegate.save_window_placement_count()); |
| } |
| } |
| |
| TEST_F(WidgetTest, WidgetAXManagerNotInitializedWhenFlagIsOff) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->Show(); |
| |
| EXPECT_EQ(widget->ax_manager(), nullptr); |
| } |
| |
| class WidgetWithAXTree : public WidgetTest { |
| public: |
| WidgetWithAXTree() = default; |
| |
| WidgetWithAXTree(const WidgetWithAXTree&) = delete; |
| WidgetWithAXTree& operator=(const WidgetWithAXTree&) = delete; |
| |
| ~WidgetWithAXTree() override = default; |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_{ |
| features::kAccessibilityTreeForViews}; |
| }; |
| |
| TEST_F(WidgetWithAXTree, WidgetAXManagerInitializedWhenFlagIsOn) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->Show(); |
| |
| EXPECT_NE(widget->ax_manager(), nullptr); |
| } |
| |
| TEST_F(WidgetTest, RootViewAccessibilityCacheInitialized) { |
| std::unique_ptr<Widget> widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| widget->Show(); |
| |
| EXPECT_TRUE(widget->GetRootView()->GetViewAccessibility().is_initialized()); |
| } |
| |
| TEST_F(WidgetTest, ClientViewAccessibilityProperties) { |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| widget->Show(); |
| |
| ui::AXNodeData node_data; |
| widget->client_view()->GetViewAccessibility().GetAccessibleNodeData( |
| &node_data); |
| EXPECT_EQ(node_data.role, ax::mojom::Role::kClient); |
| } |
| |
| TEST_F(WidgetTest, NonClientViewAccessibilityProperties) { |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| NonClientView* non_client_view = widget->non_client_view(); |
| non_client_view->SetFrameView(CreateMinimumSizeFrameView(widget.get())); |
| widget->Show(); |
| |
| ui::AXNodeData node_data; |
| non_client_view->GetViewAccessibility().GetAccessibleNodeData(&node_data); |
| EXPECT_EQ(node_data.role, ax::mojom::Role::kClient); |
| |
| node_data = ui::AXNodeData(); |
| non_client_view->frame_view()->GetViewAccessibility().GetAccessibleNodeData( |
| &node_data); |
| EXPECT_EQ(node_data.role, ax::mojom::Role::kClient); |
| } |
| |
| TEST_F(WidgetTest, UpdateAccessibleURLForRootView) { |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| widget->Show(); |
| |
| const GURL test_url("https://example.com"); |
| widget->UpdateAccessibleURLForRootView(test_url); |
| |
| ui::AXNodeData node_data; |
| widget->GetRootView()->GetViewAccessibility().GetAccessibleNodeData( |
| &node_data); |
| EXPECT_EQ(node_data.GetStringAttribute(ax::mojom::StringAttribute::kUrl), |
| test_url); |
| } |
| |
| class WidgetChildObserver : public WidgetObserver { |
| public: |
| explicit WidgetChildObserver(Widget* widget) { observation_.Observe(widget); } |
| |
| const Widget* child_widget() const { return child_widget_; } |
| |
| private: |
| // WidgetObserver: |
| void OnWidgetChildAdded(Widget* widget, Widget* child_widget) override { |
| child_widget_ = child_widget; |
| WidgetObserver::OnWidgetChildAdded(widget, child_widget); |
| } |
| |
| void OnWidgetChildRemoved(Widget* widget, Widget* child_widget) override { |
| EXPECT_EQ(widget, observation_.GetSource()); |
| EXPECT_EQ(child_widget, child_widget_); |
| child_widget_ = nullptr; |
| WidgetObserver::OnWidgetChildRemoved(widget, child_widget); |
| } |
| |
| base::ScopedObservation<Widget, WidgetObserver> observation_{this}; |
| raw_ptr<Widget> child_widget_ = nullptr; |
| }; |
| |
| TEST_F(WidgetTest, ChildWidgetNotifiesObserverWhenInitializedAndDestroyed) { |
| // Adding a child widget should call back the observer. |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| WidgetChildObserver observer(widget.get()); |
| std::unique_ptr<Widget> child_widget = |
| base::WrapUnique(CreateChildPlatformWidget( |
| widget->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| EXPECT_EQ(observer.child_widget(), child_widget.get()); |
| |
| // Destroy the child and verify that the observer was notified. |
| child_widget.reset(); |
| EXPECT_EQ(observer.child_widget(), nullptr); |
| } |
| |
| TEST_F(WidgetTest, ChildWidgetNotifiesObserverWhenReparented) { |
| // Verify that reparenting a child widget notifies both the outgoing and |
| // incoming parent widgets. |
| std::unique_ptr<Widget> widget_1 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| WidgetChildObserver observer_1(widget_1.get()); |
| |
| std::unique_ptr<Widget> widget_2 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| WidgetChildObserver observer_2(widget_2.get()); |
| |
| // Create a child widget of `widget_1`. |
| std::unique_ptr<Widget> child_widget = |
| base::WrapUnique(CreateChildPlatformWidget( |
| widget_1->GetNativeView(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| EXPECT_EQ(observer_1.child_widget(), child_widget.get()); |
| |
| Widget::ReparentNativeView(child_widget->GetNativeView(), |
| widget_2->GetNativeView()); |
| EXPECT_EQ(observer_1.child_widget(), nullptr); |
| EXPECT_EQ(observer_2.child_widget(), child_widget.get()); |
| |
| child_widget.reset(); |
| EXPECT_EQ(observer_2.child_widget(), nullptr); |
| } |
| |
| TEST_F(WidgetTest, NativeWidgetNotifiedOfWidgetDestructionForClientOwnsWidget) { |
| auto widget = std::make_unique<Widget>(); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| |
| auto native_widget = |
| std::make_unique<testing::NiceMock<MockNativeWidget>>(widget.get()); |
| ON_CALL(*native_widget, CreateNonClientFrameView).WillByDefault([]() { |
| return std::make_unique<NonClientFrameView>(); |
| }); |
| params.native_widget = native_widget.get(); |
| widget->Init(std::move(params)); |
| |
| EXPECT_CALL(*native_widget, ClientDestroyedWidget()); |
| widget.reset(); |
| } |
| |
| // Parameterized test that verifies the behavior of SetAspectRatio with respect |
| // to the excluded margin. |
| class WidgetSetAspectRatioTest |
| : public ViewsTestBase, |
| public testing::WithParamInterface<gfx::Size /* margin */> { |
| public: |
| WidgetSetAspectRatioTest() : margin_(GetParam()) {} |
| |
| WidgetSetAspectRatioTest(const WidgetSetAspectRatioTest&) = delete; |
| WidgetSetAspectRatioTest& operator=(const WidgetSetAspectRatioTest&) = delete; |
| |
| ~WidgetSetAspectRatioTest() override = default; |
| |
| // ViewsTestBase: |
| void SetUp() override { |
| ViewsTestBase::SetUp(); |
| widget_ = std::make_unique<Widget>(); |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); |
| native_widget_ = std::make_unique<MockNativeWidget>(widget()); |
| ON_CALL(*native_widget(), CreateNonClientFrameView).WillByDefault([this]() { |
| auto frame_view_with_fixed_margin = |
| std::make_unique<test::ConfigurableTestFrameView>(widget_.get()); |
| frame_view_with_fixed_margin->set_client_view_margin(margin()); |
| return frame_view_with_fixed_margin; |
| }); |
| params.native_widget = native_widget(); |
| widget()->Init(std::move(params)); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| void TearDown() override { |
| // `ViewAccessibility` objects have some references to the `widget` which |
| // must be updated when the widget is freed. The function that is in charge |
| // of clearing these lists however (`OnNativeWidgetDestroying`), is never |
| // called in this test suite because we use a `MockNativeWindow` rather than |
| // a `NativeWindow`. So we make sure this clean up happens manually. |
| widget()->OnNativeWidgetDestroying(); |
| native_widget_.reset(); |
| widget()->Close(); |
| widget_.reset(); |
| ViewsTestBase::TearDown(); |
| } |
| |
| const gfx::Size& margin() const { return margin_; } |
| Widget* widget() { return widget_.get(); } |
| MockNativeWidget* native_widget() { return native_widget_.get(); } |
| |
| private: |
| // Margin around the client view that should be excluded. |
| const gfx::Size margin_; |
| std::unique_ptr<Widget> widget_; |
| std::unique_ptr<MockNativeWidget> native_widget_; |
| }; |
| |
| TEST_P(WidgetSetAspectRatioTest, SetAspectRatioIncludesMargin) { |
| // Provide a nonzero size. It doesn't particularly matter what, as long as |
| // it's larger than our margin. |
| const gfx::Rect root_view_bounds(0, 0, 100, 200); |
| ASSERT_GT(root_view_bounds.width(), margin().width()); |
| ASSERT_GT(root_view_bounds.height(), margin().height()); |
| widget()->non_client_view()->SetBoundsRect(root_view_bounds); |
| |
| // Verify that the excluded margin matches the margin that our custom |
| // non-client frame provides. |
| const gfx::SizeF aspect_ratio(1.5f, 1.0f); |
| EXPECT_CALL(*native_widget(), SetAspectRatio(aspect_ratio, margin())); |
| widget()->SetAspectRatio(aspect_ratio); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(WidgetSetAspectRatioTestInstantiation, |
| WidgetSetAspectRatioTest, |
| ::testing::Values(gfx::Size(15, 20), gfx::Size(0, 0))); |
| |
| class WidgetShadowTest : public WidgetTest { |
| public: |
| WidgetShadowTest() = default; |
| |
| WidgetShadowTest(const WidgetShadowTest&) = delete; |
| WidgetShadowTest& operator=(const WidgetShadowTest&) = delete; |
| |
| ~WidgetShadowTest() override = default; |
| |
| // WidgetTest: |
| void SetUp() override { |
| set_native_widget_type(NativeWidgetType::kDesktop); |
| WidgetTest::SetUp(); |
| InitControllers(); |
| } |
| |
| void TearDown() override { |
| #if defined(USE_AURA) && !BUILDFLAG(ENABLE_DESKTOP_AURA) |
| shadow_controller_.reset(); |
| focus_controller_.reset(); |
| #endif |
| WidgetTest::TearDown(); |
| } |
| |
| Widget::InitParams CreateParams(Widget::InitParams::Ownership ownership, |
| Widget::InitParams::Type type) override { |
| Widget::InitParams params = |
| WidgetTest::CreateParams(ownership, override_type_.value_or(type)); |
| params.shadow_type = Widget::InitParams::ShadowType::kDrop; |
| params.shadow_elevation = 10; |
| params.name = name_; |
| params.child = force_child_; |
| return params; |
| } |
| |
| protected: |
| std::optional<Widget::InitParams::Type> override_type_; |
| std::string name_; |
| bool force_child_ = false; |
| |
| private: |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| void InitControllers() {} |
| #else |
| class TestFocusRules : public wm::BaseFocusRules { |
| public: |
| TestFocusRules() = default; |
| |
| TestFocusRules(const TestFocusRules&) = delete; |
| TestFocusRules& operator=(const TestFocusRules&) = delete; |
| |
| bool SupportsChildActivation(const aura::Window* window) const override { |
| return true; |
| } |
| }; |
| |
| void InitControllers() { |
| focus_controller_ = |
| std::make_unique<wm::FocusController>(new TestFocusRules); |
| shadow_controller_ = std::make_unique<wm::ShadowController>( |
| focus_controller_.get(), nullptr); |
| } |
| |
| std::unique_ptr<wm::FocusController> focus_controller_; |
| std::unique_ptr<wm::ShadowController> shadow_controller_; |
| #endif // !BUILDFLAG(ENABLE_DESKTOP_AURA) && !BUILDFLAG(IS_MAC) |
| }; |
| |
| // Disabled on Mac: All drop shadows are managed out of process for now. |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_ShadowsInRootWindow DISABLED_ShadowsInRootWindow |
| #else |
| #define MAYBE_ShadowsInRootWindow ShadowsInRootWindow |
| #endif |
| |
| // Test that shadows are not added to root windows when created or upon |
| // activation. Test that shadows are added to non-root windows even if not |
| // activated. |
| TEST_F(WidgetShadowTest, MAYBE_ShadowsInRootWindow) { |
| #if defined(USE_AURA) && !BUILDFLAG(ENABLE_DESKTOP_AURA) |
| // On ChromeOS, top-levels have shadows. |
| bool top_level_window_should_have_shadow = true; |
| #else |
| // On non-chromeos platforms, the hosting OS is responsible for the shadow. |
| bool top_level_window_should_have_shadow = false; |
| #endif |
| |
| // To start, just create a Widget. This constructs the first ShadowController |
| // which will start observing the environment for additional aura::Window |
| // initialization. The very first ShadowController in DesktopNativeWidgetAura |
| // is created after the call to aura::Window::Init(), so the ShadowController |
| // Impl class won't ever see this first Window being initialized. |
| name_ = "other_top_level"; |
| Widget* other_top_level = CreateTopLevelNativeWidget(); |
| |
| name_ = "top_level"; |
| Widget* top_level = CreateTopLevelNativeWidget(); |
| top_level->SetBounds(gfx::Rect(100, 100, 320, 200)); |
| |
| EXPECT_FALSE(WidgetHasInProcessShadow(top_level)); |
| EXPECT_FALSE(top_level->IsVisible()); |
| top_level->ShowInactive(); |
| EXPECT_EQ(top_level_window_should_have_shadow, |
| WidgetHasInProcessShadow(top_level)); |
| top_level->Show(); |
| EXPECT_EQ(top_level_window_should_have_shadow, |
| WidgetHasInProcessShadow(top_level)); |
| |
| name_ = "control"; |
| Widget* control = CreateChildNativeWidgetWithParent(top_level); |
| control->SetBounds(gfx::Rect(20, 20, 160, 100)); |
| |
| // Widgets of TYPE_CONTROL become visible during Init, so start with a shadow. |
| EXPECT_TRUE(WidgetHasInProcessShadow(control)); |
| control->ShowInactive(); |
| EXPECT_TRUE(WidgetHasInProcessShadow(control)); |
| control->Show(); |
| EXPECT_TRUE(WidgetHasInProcessShadow(control)); |
| |
| name_ = "child"; |
| override_type_ = Widget::InitParams::TYPE_POPUP; |
| force_child_ = true; |
| Widget* child = CreateChildNativeWidgetWithParent(top_level); |
| child->SetBounds(gfx::Rect(20, 20, 160, 100)); |
| |
| // Now false: the Widget hasn't been shown yet. |
| EXPECT_FALSE(WidgetHasInProcessShadow(child)); |
| child->ShowInactive(); |
| EXPECT_TRUE(WidgetHasInProcessShadow(child)); |
| child->Show(); |
| EXPECT_TRUE(WidgetHasInProcessShadow(child)); |
| |
| other_top_level->Show(); |
| |
| // Re-activate the top level window. This handles a hypothetical case where |
| // a shadow is added via the ActivationChangeObserver rather than by the |
| // aura::WindowObserver. Activation changes only modify an existing shadow |
| // (if there is one), but should never install a Shadow, even if the Window |
| // properties otherwise say it should have one. |
| top_level->Show(); |
| EXPECT_EQ(top_level_window_should_have_shadow, |
| WidgetHasInProcessShadow(top_level)); |
| |
| top_level->Close(); |
| other_top_level->Close(); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| |
| // Tests the case where an intervening owner popup window is destroyed out from |
| // under the currently active modal top-level window. In this instance, the |
| // remaining top-level windows should be re-enabled. |
| TEST_F(DesktopWidgetTest, WindowModalOwnerDestroyedEnabledTest) { |
| // top_level_widget owns owner_dialog_widget which owns owned_dialog_widget. |
| std::unique_ptr<Widget> top_level_widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| top_level_widget->Show(); |
| |
| // Create the owner modal dialog. |
| const auto create_params = [this](Widget* widget, gfx::NativeView parent) { |
| Widget::InitParams init_params = |
| CreateParamsForTestWidget(Widget::InitParams::TYPE_WINDOW); |
| init_params.delegate = new WidgetDelegate(); |
| init_params.delegate->SetModalType(ui::mojom::ModalType::kWindow); |
| init_params.parent = parent; |
| init_params.native_widget = |
| new test::TestPlatformNativeWidget<DesktopNativeWidgetAura>( |
| widget, false, nullptr); |
| return init_params; |
| }; |
| Widget owner_dialog_widget; |
| owner_dialog_widget.Init( |
| create_params(&owner_dialog_widget, top_level_widget->GetNativeView())); |
| owner_dialog_widget.Show(); |
| HWND owner_hwnd = HWNDForWidget(&owner_dialog_widget); |
| |
| // Create the owned modal dialog. |
| Widget owned_dialog_widget; |
| owned_dialog_widget.Init( |
| create_params(&owned_dialog_widget, owner_dialog_widget.GetNativeView())); |
| owned_dialog_widget.Show(); |
| HWND owned_hwnd = HWNDForWidget(&owned_dialog_widget); |
| |
| RunPendingMessages(); |
| |
| HWND top_hwnd = HWNDForWidget(top_level_widget.get()); |
| |
| EXPECT_FALSE(!!IsWindowEnabled(owner_hwnd)); |
| EXPECT_FALSE(!!IsWindowEnabled(top_hwnd)); |
| EXPECT_TRUE(!!IsWindowEnabled(owned_hwnd)); |
| |
| owner_dialog_widget.CloseNow(); |
| RunPendingMessages(); |
| |
| EXPECT_FALSE(!!IsWindow(owner_hwnd)); |
| EXPECT_FALSE(!!IsWindow(owned_hwnd)); |
| EXPECT_TRUE(!!IsWindowEnabled(top_hwnd)); |
| |
| top_level_widget->CloseNow(); |
| } |
| |
| TEST_F(DesktopWidgetTest, StackAboveTest) { |
| WidgetAutoclosePtr root_one(CreateTopLevelNativeWidget()); |
| WidgetAutoclosePtr root_two(CreateTopLevelNativeWidget()); |
| Widget* child_one = CreateChildNativeWidgetWithParent(root_one->AsWidget()); |
| Widget* child_one_b = CreateChildNativeWidgetWithParent(root_one->AsWidget()); |
| Widget* child_two = CreateChildNativeWidgetWithParent(root_two->AsWidget()); |
| Widget* grandchild_one = |
| CreateChildNativeWidgetWithParent(child_one->AsWidget()); |
| Widget* grandchild_two = |
| CreateChildNativeWidgetWithParent(child_two->AsWidget()); |
| |
| root_one->ShowInactive(); |
| child_one->ShowInactive(); |
| child_one_b->ShowInactive(); |
| grandchild_one->ShowInactive(); |
| root_two->ShowInactive(); |
| child_two->ShowInactive(); |
| grandchild_two->ShowInactive(); |
| |
| // Creates the following where Z-Order is from Left to Right. |
| // root_one root_two |
| // / \ / |
| // child_one_b child_one child_two |
| // / / |
| // grandchild_one grandchild_two |
| // |
| // Note: child_one and grandchild_one were brought to front |
| // when grandchild_one was shown. |
| |
| // Child elements are stacked above parent. |
| EXPECT_TRUE(child_one->IsStackedAbove(root_one->GetNativeView())); |
| EXPECT_TRUE(child_one_b->IsStackedAbove(root_one->GetNativeView())); |
| EXPECT_TRUE(grandchild_one->IsStackedAbove(child_one->GetNativeView())); |
| EXPECT_TRUE(grandchild_two->IsStackedAbove(root_two->GetNativeView())); |
| |
| // Siblings with higher z-order are stacked correctly. |
| EXPECT_TRUE(child_one->IsStackedAbove(child_one_b->GetNativeView())); |
| EXPECT_TRUE(grandchild_one->IsStackedAbove(child_one_b->GetNativeView())); |
| |
| // Root elements are stacked above child of a root with lower z-order. |
| EXPECT_TRUE(root_two->IsStackedAbove(root_one->GetNativeView())); |
| EXPECT_TRUE(root_two->IsStackedAbove(child_one_b->GetNativeView())); |
| |
| // Child elements are stacked above child of root with lower z-order. |
| EXPECT_TRUE(child_two->IsStackedAbove(child_one_b->GetNativeView())); |
| EXPECT_TRUE(child_two->IsStackedAbove(grandchild_one->GetNativeView())); |
| EXPECT_TRUE(grandchild_two->IsStackedAbove(child_one->GetNativeView())); |
| EXPECT_TRUE(grandchild_two->IsStackedAbove(root_one->GetNativeView())); |
| |
| // False cases to verify function is not just returning true for all cases. |
| EXPECT_FALSE(root_one->IsStackedAbove(grandchild_two->GetNativeView())); |
| EXPECT_FALSE(root_one->IsStackedAbove(grandchild_one->GetNativeView())); |
| EXPECT_FALSE(child_two->IsStackedAbove(grandchild_two->GetNativeView())); |
| EXPECT_FALSE(child_one->IsStackedAbove(grandchild_two->GetNativeView())); |
| EXPECT_FALSE(child_one_b->IsStackedAbove(child_two->GetNativeView())); |
| EXPECT_FALSE(grandchild_one->IsStackedAbove(grandchild_two->GetNativeView())); |
| EXPECT_FALSE(grandchild_one->IsStackedAbove(root_two->GetNativeView())); |
| EXPECT_FALSE(child_one_b->IsStackedAbove(grandchild_one->GetNativeView())); |
| } |
| |
| #endif // BUILDFLAG(IS_WIN) |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| |
| namespace { |
| |
| class CompositingWidgetTest : public DesktopWidgetTest { |
| public: |
| CompositingWidgetTest() |
| : widget_types_{Widget::InitParams::TYPE_WINDOW, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS, |
| Widget::InitParams::TYPE_CONTROL, |
| Widget::InitParams::TYPE_POPUP, |
| Widget::InitParams::TYPE_MENU, |
| Widget::InitParams::TYPE_TOOLTIP, |
| Widget::InitParams::TYPE_BUBBLE, |
| Widget::InitParams::TYPE_DRAG} {} |
| |
| CompositingWidgetTest(const CompositingWidgetTest&) = delete; |
| CompositingWidgetTest& operator=(const CompositingWidgetTest&) = delete; |
| |
| ~CompositingWidgetTest() override = default; |
| |
| Widget::InitParams CreateParams(Widget::InitParams::Ownership ownership, |
| Widget::InitParams::Type type) override { |
| Widget::InitParams params = |
| DesktopWidgetTest::CreateParams(ownership, type); |
| params.opacity = opacity_; |
| return params; |
| } |
| |
| void CheckAllWidgetsForOpacity( |
| const Widget::InitParams::WindowOpacity opacity) { |
| opacity_ = opacity; |
| for (const auto& widget_type : widget_types_) { |
| #if BUILDFLAG(IS_MAC) |
| // Tooltips are native on Mac. See NativeWidgetNSWindowBridge::Init. |
| if (widget_type == Widget::InitParams::TYPE_TOOLTIP) { |
| continue; |
| } |
| #elif BUILDFLAG(IS_WIN) |
| // Other widget types would require to create a parent window and the |
| // the purpose of this test is mainly X11 in the first place. |
| if (widget_type != Widget::InitParams::TYPE_WINDOW) { |
| continue; |
| } |
| #endif |
| std::unique_ptr<Widget> widget = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET, widget_type); |
| |
| // Use NativeWidgetAura directly. |
| if (widget_type == Widget::InitParams::TYPE_WINDOW_FRAMELESS || |
| widget_type == Widget::InitParams::TYPE_CONTROL) { |
| continue; |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| // Mac always always has a compositing window manager, but doesn't have |
| // transparent titlebars which is what ShouldWindowContentsBeTransparent() |
| // is currently used for. Asking for transparency should get it. Note that |
| // TestViewsDelegate::use_transparent_windows_ determines the result of |
| // kInferOpacity: assume it is false. |
| bool should_be_transparent = |
| opacity_ == Widget::InitParams::WindowOpacity::kTranslucent; |
| #else |
| bool should_be_transparent = widget->ShouldWindowContentsBeTransparent(); |
| #endif |
| |
| EXPECT_EQ(IsNativeWindowTransparent(widget->GetNativeWindow()), |
| should_be_transparent); |
| } |
| } |
| |
| protected: |
| const std::vector<Widget::InitParams::Type> widget_types_; |
| Widget::InitParams::WindowOpacity opacity_ = |
| Widget::InitParams::WindowOpacity::kInferred; |
| }; |
| |
| } // namespace |
| |
| // Only test manually set opacity via kOpaque or kTranslucent. kInferred is |
| // unpredictable and depends on the platform and window type. |
| TEST_F(CompositingWidgetTest, Transparency_DesktopWidgetOpaque) { |
| CheckAllWidgetsForOpacity(Widget::InitParams::WindowOpacity::kOpaque); |
| } |
| |
| TEST_F(CompositingWidgetTest, Transparency_DesktopWidgetTranslucent) { |
| CheckAllWidgetsForOpacity(Widget::InitParams::WindowOpacity::kTranslucent); |
| } |
| |
| namespace { |
| |
| class ScreenshotWidgetTest : public ViewsTestBase { |
| public: |
| ScreenshotWidgetTest() = default; |
| |
| // ViewsTestBase: |
| void SetUp() override { |
| ViewsTestBase::SetUp(); |
| widget_ = std::make_unique<Widget>(); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW); |
| native_widget_ = |
| std::make_unique<testing::NiceMock<MockNativeWidget>>(widget()); |
| EXPECT_CALL(*native_widget(), SetAllowScreenshots(_)) |
| .WillOnce([this](bool allowed) { |
| screenshots_allowed_ = allowed; |
| return true; |
| }); |
| params.native_widget = native_widget(); |
| widget()->Init(std::move(params)); |
| } |
| |
| void TearDown() override { |
| // `ViewAccessibility` objects have some references to the `widget` which |
| // must be updated when the widget is freed. The function that is in charge |
| // of clearing these lists however (`OnNativeWidgetDestroying`), is never |
| // called in this test suite because we use a `MockNativeWindow` rather than |
| // a `NativeWindow`. So we make sure this clean up happens manually. |
| widget()->OnNativeWidgetDestroying(); |
| native_widget_.reset(); |
| ViewsTestBase::TearDown(); |
| } |
| |
| Widget* widget() { return widget_.get(); } |
| MockNativeWidget* native_widget() { return native_widget_.get(); } |
| const std::optional<bool>& screenshots_allowed() { |
| return screenshots_allowed_; |
| } |
| |
| private: |
| std::unique_ptr<Widget> widget_; |
| std::unique_ptr<MockNativeWidget> native_widget_; |
| std::optional<bool> screenshots_allowed_; |
| }; |
| |
| } // namespace |
| |
| TEST_F(ScreenshotWidgetTest, CallsNativeWidget) { |
| widget()->SetAllowScreenshots(false); |
| ASSERT_TRUE(screenshots_allowed().has_value()); |
| EXPECT_FALSE(screenshots_allowed().value()); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| |
| namespace { |
| |
| class WidgetModalVisibilityObserver : public WidgetObserver { |
| public: |
| MOCK_METHOD(void, |
| OnWidgetWindowModalVisibilityChanged, |
| (Widget*, bool), |
| (override)); |
| }; |
| |
| } // namespace |
| |
| TEST_F(WidgetTest, ChildWidgetNotifiesModalVisibilityChanged) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| testing::NiceMock<WidgetModalVisibilityObserver> observer; |
| base::ScopedObservation<Widget, WidgetObserver> observation(&observer); |
| observation.Observe(widget.get()); |
| widget->Show(); |
| |
| auto child_widget_delegate = std::make_unique<WidgetDelegate>(); |
| auto child_widget = std::make_unique<Widget>(); |
| child_widget_delegate->SetModalType(ui::mojom::ModalType::kWindow); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| params.delegate = child_widget_delegate.get(); |
| params.parent = widget->GetNativeView(); |
| child_widget->Init(std::move(params)); |
| |
| // Sequence: show -> hide -> show -> destroy. |
| testing::InSequence seq; |
| EXPECT_CALL(observer, |
| OnWidgetWindowModalVisibilityChanged(widget.get(), true)); |
| EXPECT_CALL(observer, |
| OnWidgetWindowModalVisibilityChanged(widget.get(), false)); |
| EXPECT_CALL(observer, |
| OnWidgetWindowModalVisibilityChanged(widget.get(), true)); |
| EXPECT_CALL(observer, |
| OnWidgetWindowModalVisibilityChanged(widget.get(), false)); |
| |
| WidgetVisibleWaiter waiter(child_widget.get()); |
| child_widget->Show(); |
| waiter.Wait(); |
| child_widget->Hide(); |
| waiter.WaitUntilInvisible(); |
| child_widget->Show(); |
| waiter.Wait(); |
| // Destroy the child widget, the parent should be notified about child modal |
| // visibility change. |
| child_widget.reset(); |
| // No need to wait for visibility change because the widget is already |
| // destroyed. |
| |
| widget->RemoveObserver(&observer); |
| } |
| |
| TEST_F(WidgetTest, RemoveClientContentsView) { |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| auto client_view = widget->RemoveClientContentsView<View>(); |
| EXPECT_TRUE(client_view); |
| EXPECT_FALSE(widget->GetClientContentsView()); |
| } |
| |
| TEST_F(WidgetTest, SetClientContentsView) { |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| auto client_view = widget->RemoveClientContentsView<View>(); |
| EXPECT_TRUE(client_view); |
| auto* client_view_ptr = widget->SetClientContentsView(std::move(client_view)); |
| EXPECT_EQ(client_view_ptr, widget->GetClientContentsView()); |
| } |
| |
| TEST_F(WidgetTest, ReplaceClientContentsView) { |
| std::unique_ptr<Widget> widget = CreateTestWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| auto client_view = std::make_unique<ContentsView>(); |
| EXPECT_NE(client_view.get(), widget->GetClientContentsView()); |
| auto* client_view_ptr = widget->SetClientContentsView(std::move(client_view)); |
| EXPECT_EQ(client_view_ptr, widget->GetClientContentsView()); |
| } |
| |
| } // namespace views::test |