| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/renderer_host/cursor_manager.h" |
| #include <memory> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "build/build_config.h" |
| #include "content/browser/renderer_host/agent_scheduling_group_host.h" |
| #include "content/browser/renderer_host/mock_render_widget_host.h" |
| #include "content/browser/renderer_host/render_widget_host_impl.h" |
| #include "content/common/cursors/webcursor.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "content/public/test/mock_render_process_host.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "content/test/mock_render_widget_host_delegate.h" |
| #include "content/test/test_render_view_host.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/cursor/mojom/cursor_type.mojom-shared.h" |
| |
| // CursorManager is only instantiated on Aura and Mac. |
| #if defined(USE_AURA) || BUILDFLAG(IS_MAC) |
| |
| namespace content { |
| |
| namespace { |
| |
| class MockRenderWidgetHostViewForCursors : public TestRenderWidgetHostView { |
| public: |
| MockRenderWidgetHostViewForCursors(RenderWidgetHost* host, bool top_view) |
| : TestRenderWidgetHostView(host) { |
| if (top_view) |
| cursor_manager_ = std::make_unique<CursorManager>(this); |
| } |
| |
| void DisplayCursor(const WebCursor& cursor) override { |
| current_cursor_ = cursor; |
| } |
| |
| CursorManager* GetCursorManager() override { return cursor_manager_.get(); } |
| |
| const WebCursor& cursor() { return current_cursor_; } |
| |
| private: |
| WebCursor current_cursor_; |
| std::unique_ptr<CursorManager> cursor_manager_; |
| }; |
| |
| class CursorManagerTest : public testing::Test { |
| public: |
| CursorManagerTest() = default; |
| |
| CursorManagerTest(const CursorManagerTest&) = delete; |
| CursorManagerTest& operator=(const CursorManagerTest&) = delete; |
| |
| void SetUp() override { |
| browser_context_ = std::make_unique<TestBrowserContext>(); |
| process_host_ = |
| std::make_unique<MockRenderProcessHost>(browser_context_.get()); |
| agent_scheduling_group_host_ = |
| std::make_unique<AgentSchedulingGroupHost>(*process_host_); |
| widget_host_ = MakeNewWidgetHost(); |
| top_view_ = |
| new MockRenderWidgetHostViewForCursors(widget_host_.get(), true); |
| } |
| |
| std::unique_ptr<RenderWidgetHostImpl> MakeNewWidgetHost() { |
| int32_t routing_id = process_host_->GetNextRoutingID(); |
| return MockRenderWidgetHost::Create( |
| /*frame_tree=*/nullptr, &delegate_, *agent_scheduling_group_host_, |
| routing_id); |
| } |
| |
| void TearDown() override { |
| if (top_view_) |
| delete top_view_; |
| |
| widget_host_ = nullptr; |
| process_host_->Cleanup(); |
| agent_scheduling_group_host_ = nullptr; |
| process_host_ = nullptr; |
| } |
| |
| protected: |
| BrowserTaskEnvironment task_environment_; |
| |
| std::unique_ptr<BrowserContext> browser_context_; |
| std::unique_ptr<MockRenderProcessHost> process_host_; |
| std::unique_ptr<AgentSchedulingGroupHost> agent_scheduling_group_host_; |
| std::unique_ptr<RenderWidgetHostImpl> widget_host_; |
| |
| // Tests should set this to nullptr if they've already triggered its |
| // destruction. |
| raw_ptr<MockRenderWidgetHostViewForCursors> top_view_; |
| |
| MockRenderWidgetHostDelegate delegate_; |
| }; |
| |
| } // namespace |
| |
| // Verify basic CursorManager functionality when no OOPIFs are present. |
| TEST_F(CursorManagerTest, CursorOnSingleView) { |
| // Simulate mouse over the top-level frame without an UpdateCursor message. |
| top_view_->GetCursorManager()->UpdateViewUnderCursor(top_view_); |
| |
| // The view should be using the default cursor. |
| EXPECT_EQ(top_view_->cursor(), WebCursor()); |
| |
| WebCursor cursor_hand(ui::mojom::CursorType::kHand); |
| |
| // Update the view with a non-default cursor. |
| top_view_->GetCursorManager()->UpdateCursor(top_view_, cursor_hand); |
| |
| // Verify the RenderWidgetHostView now uses the correct cursor. |
| EXPECT_EQ(top_view_->cursor(), cursor_hand); |
| } |
| |
| // Verify cursor interactions between a parent frame and an out-of-process |
| // child frame. |
| TEST_F(CursorManagerTest, CursorOverChildView) { |
| std::unique_ptr<RenderWidgetHostImpl> widget_host(MakeNewWidgetHost()); |
| std::unique_ptr<MockRenderWidgetHostViewForCursors> child_view( |
| new MockRenderWidgetHostViewForCursors(widget_host.get(), false)); |
| |
| WebCursor cursor_hand(ui::mojom::CursorType::kHand); |
| |
| // Set the child frame's cursor to a hand. This should not propagate to the |
| // top-level view without the mouse moving over the child frame. |
| top_view_->GetCursorManager()->UpdateCursor(child_view.get(), cursor_hand); |
| EXPECT_NE(top_view_->cursor(), cursor_hand); |
| |
| // Now moving the mouse over the child frame should update the overall cursor. |
| top_view_->GetCursorManager()->UpdateViewUnderCursor(child_view.get()); |
| EXPECT_EQ(top_view_->cursor(), cursor_hand); |
| |
| // Destruction of the child view should restore the parent frame's cursor. |
| top_view_->GetCursorManager()->ViewBeingDestroyed(child_view.get()); |
| EXPECT_NE(top_view_->cursor(), cursor_hand); |
| } |
| |
| // Verify interactions between two independent OOPIFs, including interleaving |
| // cursor updates and mouse movements. This simulates potential race |
| // conditions between cursor updates. |
| TEST_F(CursorManagerTest, CursorOverMultipleChildViews) { |
| std::unique_ptr<RenderWidgetHostImpl> widget_host1(MakeNewWidgetHost()); |
| std::unique_ptr<MockRenderWidgetHostViewForCursors> child_view1( |
| new MockRenderWidgetHostViewForCursors(widget_host1.get(), false)); |
| std::unique_ptr<RenderWidgetHostImpl> widget_host2(MakeNewWidgetHost()); |
| std::unique_ptr<MockRenderWidgetHostViewForCursors> child_view2( |
| new MockRenderWidgetHostViewForCursors(widget_host2.get(), false)); |
| |
| WebCursor cursor_hand(ui::mojom::CursorType::kHand); |
| |
| WebCursor cursor_cross(ui::mojom::CursorType::kCross); |
| |
| WebCursor cursor_pointer(ui::mojom::CursorType::kPointer); |
| |
| // Initialize each View to a different cursor. |
| top_view_->GetCursorManager()->UpdateCursor(top_view_, cursor_hand); |
| top_view_->GetCursorManager()->UpdateCursor(child_view1.get(), cursor_cross); |
| top_view_->GetCursorManager()->UpdateCursor(child_view2.get(), |
| cursor_pointer); |
| EXPECT_EQ(top_view_->cursor(), cursor_hand); |
| |
| // Simulate moving the mouse between child views and receiving cursor updates. |
| top_view_->GetCursorManager()->UpdateViewUnderCursor(child_view1.get()); |
| EXPECT_EQ(top_view_->cursor(), cursor_cross); |
| top_view_->GetCursorManager()->UpdateViewUnderCursor(child_view2.get()); |
| EXPECT_EQ(top_view_->cursor(), cursor_pointer); |
| |
| // Simulate cursor updates to both child views and the parent view. An |
| // update to child_view1 or the parent view should not change the current |
| // cursor because the mouse is over child_view2. |
| top_view_->GetCursorManager()->UpdateCursor(child_view1.get(), cursor_hand); |
| EXPECT_EQ(top_view_->cursor(), cursor_pointer); |
| top_view_->GetCursorManager()->UpdateCursor(child_view2.get(), cursor_cross); |
| EXPECT_EQ(top_view_->cursor(), cursor_cross); |
| top_view_->GetCursorManager()->UpdateCursor(top_view_, cursor_hand); |
| EXPECT_EQ(top_view_->cursor(), cursor_cross); |
| |
| // Similarly, destroying child_view1 should have no effect on the cursor, |
| // but destroying child_view2 should change it. |
| top_view_->GetCursorManager()->ViewBeingDestroyed(child_view1.get()); |
| EXPECT_EQ(top_view_->cursor(), cursor_cross); |
| top_view_->GetCursorManager()->ViewBeingDestroyed(child_view2.get()); |
| EXPECT_EQ(top_view_->cursor(), cursor_hand); |
| } |
| |
| } // namespace content |
| |
| #endif // defined(USE_AURA) || BUILDFLAG(IS_MAC) |