| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/test/bind.h" |
| #include "base/test/gtest_util.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/browser/ui/browser_element_identifiers.h" |
| #include "chrome/browser/ui/browser_navigator.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/toolbar/app_menu_model.h" |
| #include "chrome/browser/ui/views/bubble/webui_bubble_dialog_view.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/interaction/interaction_test_util_browser.h" |
| #include "chrome/test/interaction/tracked_element_webcontents.h" |
| #include "chrome/test/interaction/webcontents_interaction_test_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/interaction/element_identifier.h" |
| #include "ui/base/interaction/element_tracker.h" |
| #include "ui/base/interaction/expect_call_in_scope.h" |
| #include "ui/base/interaction/interaction_sequence.h" |
| #include "ui/base/page_transition_types.h" |
| #include "ui/base/test/ui_controls.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/controls/webview/webview.h" |
| #include "ui/views/interaction/element_tracker_views.h" |
| #include "ui/views/view_utils.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTabSearchPageElementId); |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kWebContentsElementId); |
| constexpr char kDocumentWithButtonURL[] = "/button.html"; |
| constexpr char kDocumentWithIframe[] = "/iframe_elements.html"; |
| } // namespace |
| |
| class WebContentsInteractionTestUtilInteractiveUiTest |
| : public InProcessBrowserTest { |
| public: |
| WebContentsInteractionTestUtilInteractiveUiTest() = default; |
| ~WebContentsInteractionTestUtilInteractiveUiTest() override = default; |
| |
| void SetUp() override { |
| set_open_about_blank_on_browser_launch(true); |
| ASSERT_TRUE(embedded_test_server()->InitializeAndListen()); |
| InProcessBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| InProcessBrowserTest::SetUpOnMainThread(); |
| embedded_test_server()->StartAcceptingConnections(); |
| } |
| |
| void TearDownOnMainThread() override { |
| EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete()); |
| InProcessBrowserTest::TearDownOnMainThread(); |
| } |
| |
| protected: |
| InteractionTestUtilBrowser test_util_; |
| }; |
| |
| |
| // This test checks that we can attach to a WebUI that isn't embedded in a tab. |
| IN_PROC_BROWSER_TEST_F(WebContentsInteractionTestUtilInteractiveUiTest, |
| OpenTabSearchMenuAndAccessWebUI) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| |
| std::unique_ptr<WebContentsInteractionTestUtil> tab_search_page; |
| const ui::ElementContext context = browser()->window()->GetElementContext(); |
| |
| // Poke into the doc to find something that's not at the top level, just to |
| // verify we can. |
| const WebContentsInteractionTestUtil::DeepQuery kTabSearchListQuery = { |
| "tab-search-app", "tab-search-page"}; |
| |
| auto sequence = |
| ui::InteractionSequence::Builder() |
| .SetCompletedCallback(completed.Get()) |
| .SetAbortedCallback(aborted.Get()) |
| .SetContext(context) |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kShown) |
| .SetElementID(kTabSearchButtonElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence* seq, |
| ui::TrackedElement* element) { |
| if (test_util_.PressButton(element) != |
| ui::test::ActionResult::kSucceeded) { |
| seq->FailForTesting(); |
| } |
| })) |
| .Build()) |
| .AddStep( |
| ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kShown) |
| .SetElementID(kTabSearchBubbleElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| auto* const web_view = |
| views::AsViewClass<WebUIBubbleDialogView>( |
| element->AsA<views::TrackedElementViews>() |
| ->view()) |
| ->web_view(); |
| tab_search_page = |
| WebContentsInteractionTestUtil::ForNonTabWebView( |
| web_view, kTabSearchPageElementId); |
| })) |
| .Build()) |
| .Build(); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, sequence->RunSynchronouslyForTesting()); |
| } |
| |
| // This test checks that when a WebUI is hidden, its element goes away. |
| IN_PROC_BROWSER_TEST_F(WebContentsInteractionTestUtilInteractiveUiTest, |
| OpenTabSearchMenuAndTestVisibility) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| |
| std::unique_ptr<WebContentsInteractionTestUtil> tab_search_page; |
| const ui::ElementContext context = browser()->window()->GetElementContext(); |
| raw_ptr<WebUIBubbleDialogView> bubble_view = nullptr; |
| |
| auto sequence = |
| ui::InteractionSequence::Builder() |
| .SetCompletedCallback(completed.Get()) |
| .SetAbortedCallback(aborted.Get()) |
| .SetContext(context) |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kShown) |
| .SetElementID(kTabSearchButtonElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence* seq, |
| ui::TrackedElement* element) { |
| if (test_util_.PressButton(element) != |
| ui::test::ActionResult::kSucceeded) { |
| seq->FailForTesting(); |
| } |
| })) |
| .Build()) |
| .AddStep( |
| ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kShown) |
| .SetElementID(kTabSearchBubbleElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| bubble_view = views::AsViewClass<WebUIBubbleDialogView>( |
| element->AsA<views::TrackedElementViews>()->view()); |
| tab_search_page = |
| WebContentsInteractionTestUtil::ForNonTabWebView( |
| bubble_view->web_view(), |
| kTabSearchPageElementId); |
| })) |
| .Build()) |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kShown) |
| .SetElementID(kTabSearchPageElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence* sequence, |
| ui::TrackedElement* element) { |
| // Hide the ancestor view. This should hide the |
| // whole chain and cause the element to be |
| // destroyed. |
| bubble_view->SetVisible(false); |
| })) |
| .Build()) |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kHidden) |
| .SetElementID(kTabSearchPageElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence* sequence, |
| ui::TrackedElement* element) { |
| // Verify we've also disposed of the element |
| // itself: |
| EXPECT_EQ(nullptr, |
| tab_search_page->current_element_); |
| // Show the ancestor view. This should recreate the |
| // WebUI element. |
| bubble_view->SetVisible(true); |
| })) |
| .Build()) |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kShown) |
| .SetElementID(kTabSearchPageElementId) |
| .Build()) |
| .Build(); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, sequence->RunSynchronouslyForTesting()); |
| } |
| |
| // This test verifies that the bounds of an element can be retrieved. |
| IN_PROC_BROWSER_TEST_F(WebContentsInteractionTestUtilInteractiveUiTest, |
| GetElementBoundsInScreen) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| |
| const GURL url = embedded_test_server()->GetURL(kDocumentWithButtonURL); |
| auto page = WebContentsInteractionTestUtil::ForExistingTabInBrowser( |
| browser(), kWebContentsElementId); |
| const WebContentsInteractionTestUtil::DeepQuery kButtonQuery = {"#button"}; |
| const WebContentsInteractionTestUtil::DeepQuery kTextQuery = {"#text"}; |
| |
| auto sequence = |
| ui::InteractionSequence::Builder() |
| .SetCompletedCallback(completed.Get()) |
| .SetAbortedCallback(aborted.Get()) |
| .SetContext(browser()->window()->GetElementContext()) |
| // Navigate to the test page. |
| .AddStep( |
| ui::InteractionSequence::StepBuilder() |
| .SetElementID(kWebContentsElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [url](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| auto* const owner = |
| element->AsA<TrackedElementWebContents>()->owner(); |
| owner->LoadPage(url); |
| })) |
| .Build()) |
| // Wait for the navigation to complete and check the button bounds. |
| .AddStep( |
| ui::InteractionSequence::StepBuilder() |
| .SetElementID(kWebContentsElementId) |
| .SetTransitionOnlyOnEvent(true) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&, url](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| auto* const owner = |
| element->AsA<TrackedElementWebContents>()->owner(); |
| ASSERT_EQ(url, owner->web_contents()->GetURL()); |
| |
| // Check the button bounds. |
| const gfx::Rect element_rect = |
| owner->GetElementBoundsInScreen(kButtonQuery); |
| EXPECT_FALSE(element_rect.IsEmpty()); |
| const gfx::Rect window_rect = |
| browser()->window()->GetBounds(); |
| EXPECT_TRUE(window_rect.Contains(element_rect)) |
| << "Expected window rect " << window_rect.ToString() |
| << " to contain element rect " |
| << element_rect.ToString(); |
| |
| // Verify that the text element *is* empty. |
| const gfx::Rect text_rect = |
| owner->GetElementBoundsInScreen(kTextQuery); |
| EXPECT_TRUE(text_rect.IsEmpty()); |
| })) |
| .Build()) |
| .Build(); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, sequence->RunSynchronouslyForTesting()); |
| } |
| |
| // This test verifies that the bounds of an element can be retrieved. |
| IN_PROC_BROWSER_TEST_F(WebContentsInteractionTestUtilInteractiveUiTest, |
| GetIframeElementBoundsInScreen) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| |
| const GURL url = embedded_test_server()->GetURL(kDocumentWithIframe); |
| auto page = WebContentsInteractionTestUtil::ForExistingTabInBrowser( |
| browser(), kWebContentsElementId); |
| const WebContentsInteractionTestUtil::DeepQuery kContainerQuery = { |
| "#container"}; |
| const WebContentsInteractionTestUtil::DeepQuery kIframeQuery = {"#iframe"}; |
| const WebContentsInteractionTestUtil::DeepQuery kTopElementQuery = {"#iframe", |
| "p"}; |
| const WebContentsInteractionTestUtil::DeepQuery kLinkQuery = {"#iframe", |
| "#ref"}; |
| |
| auto sequence = |
| ui::InteractionSequence::Builder() |
| .SetCompletedCallback(completed.Get()) |
| .SetAbortedCallback(aborted.Get()) |
| .SetContext(browser()->window()->GetElementContext()) |
| // Navigate to the test page. |
| .AddStep( |
| ui::InteractionSequence::StepBuilder() |
| .SetElementID(kWebContentsElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [url](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| auto* const owner = |
| element->AsA<TrackedElementWebContents>()->owner(); |
| owner->LoadPage(url); |
| })) |
| .Build()) |
| // Wait for the navigation to complete and check the button bounds. |
| .AddStep( |
| ui::InteractionSequence::StepBuilder() |
| .SetElementID(kWebContentsElementId) |
| .SetTransitionOnlyOnEvent(true) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&, url](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| auto* const owner = |
| element->AsA<TrackedElementWebContents>()->owner(); |
| const gfx::Rect window_rect = |
| browser()->window()->GetBounds(); |
| const gfx::Rect container_rect = |
| owner->GetElementBoundsInScreen(kContainerQuery); |
| |
| const gfx::Rect iframe_rect = |
| owner->GetElementBoundsInScreen(kIframeQuery); |
| EXPECT_FALSE(iframe_rect.IsEmpty()); |
| EXPECT_TRUE(window_rect.Contains(iframe_rect)) |
| << "Expected window rect " << window_rect.ToString() |
| << " to contain element rect " |
| << iframe_rect.ToString(); |
| EXPECT_TRUE(container_rect.Contains(iframe_rect)) |
| << "Expected container rect " |
| << container_rect.ToString() |
| << " to contain element rect " |
| << iframe_rect.ToString(); |
| |
| const gfx::Rect top_element_rect = |
| owner->GetElementBoundsInScreen(kTopElementQuery); |
| EXPECT_FALSE(top_element_rect.IsEmpty()); |
| EXPECT_TRUE(iframe_rect.Contains(top_element_rect)) |
| << "Expected iframe rect " << iframe_rect.ToString() |
| << " to contain element rect " |
| << top_element_rect.ToString(); |
| |
| const gfx::Rect link_rect = |
| owner->GetElementBoundsInScreen(kLinkQuery); |
| EXPECT_FALSE(link_rect.IsEmpty()); |
| EXPECT_TRUE(iframe_rect.Contains(link_rect)) |
| << "Expected iframe rect " << iframe_rect.ToString() |
| << " to contain element rect " |
| << link_rect.ToString(); |
| EXPECT_GT(link_rect.y(), top_element_rect.y()) |
| << "Expected element rect " << link_rect.ToString() |
| << " to be strictly lower on the page than the top " |
| "element " |
| << top_element_rect.ToString(); |
| })) |
| .Build()) |
| .Build(); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, sequence->RunSynchronouslyForTesting()); |
| } |
| |
| // TODO(https://crbug.com/372873264): Re-enable on chrome linux and win and mac |
| // builders. |
| #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) |
| #define MAYBE_UseElementBoundsInScreenToSendInput \ |
| DISABLED_UseElementBoundsInScreenToSendInput |
| #else |
| #define MAYBE_UseElementBoundsInScreenToSendInput \ |
| UseElementBoundsInScreenToSendInput |
| #endif |
| IN_PROC_BROWSER_TEST_F(WebContentsInteractionTestUtilInteractiveUiTest, |
| MAYBE_UseElementBoundsInScreenToSendInput) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kMouseMoveCustomEvent); |
| DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kMouseDownCustomEvent); |
| DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kMouseUpCustomEvent); |
| DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kTextVisibleCustomEvent); |
| |
| const GURL url = embedded_test_server()->GetURL(kDocumentWithButtonURL); |
| auto page = WebContentsInteractionTestUtil::ForExistingTabInBrowser( |
| browser(), kWebContentsElementId); |
| const WebContentsInteractionTestUtil::DeepQuery kButtonQuery = {"#button"}; |
| const WebContentsInteractionTestUtil::DeepQuery kTextQuery = {"#text"}; |
| |
| // This is just a convenience function for a common task in a couple of steps. |
| auto send_custom_event = [this](ui::CustomElementEventType event_type) { |
| auto* const target = |
| ui::ElementTracker::GetElementTracker()->GetUniqueElement( |
| kWebContentsElementId, browser()->window()->GetElementContext()); |
| ASSERT_NE(nullptr, target); |
| ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(target, |
| event_type); |
| }; |
| |
| auto sequence = |
| ui::InteractionSequence::Builder() |
| .SetCompletedCallback(completed.Get()) |
| .SetAbortedCallback(aborted.Get()) |
| .SetContext(browser()->window()->GetElementContext()) |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetElementID(kWebContentsElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [this, url](ui::InteractionSequence*, |
| ui::TrackedElement*) { |
| NavigateParams params(browser(), url, |
| ui::PAGE_TRANSITION_LINK); |
| Navigate(¶ms); |
| })) |
| .Build()) |
| // Wait to navigate to the test page and mouse over the button. |
| .AddStep( |
| ui::InteractionSequence::StepBuilder() |
| .SetElementID(kWebContentsElementId) |
| .SetTransitionOnlyOnEvent(true) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| auto* const owner = |
| element->AsA<TrackedElementWebContents>()->owner(); |
| ASSERT_EQ(url, owner->web_contents()->GetURL()); |
| const gfx::Rect element_rect = |
| owner->GetElementBoundsInScreen(kButtonQuery); |
| EXPECT_FALSE(element_rect.IsEmpty()); |
| const gfx::Point target = element_rect.CenterPoint(); |
| |
| display::Screen* const screen = |
| display::Screen::GetScreen(); |
| display::Display display = |
| screen->GetDisplayNearestPoint(target); |
| |
| // Move mouse to the location we calculated for the |
| // button on screen. |
| EXPECT_TRUE(ui_controls::SendMouseMoveNotifyWhenDone( |
| target.x(), target.y(), |
| base::BindLambdaForTesting([&]() { |
| send_custom_event(kMouseMoveCustomEvent); |
| }))); |
| })) |
| .Build()) |
| // Once the mouse has moved, press the left mouse button. |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kCustomEvent, |
| kMouseMoveCustomEvent) |
| .SetElementID(kWebContentsElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| EXPECT_TRUE( |
| ui_controls::SendMouseEventsNotifyWhenDone( |
| ui_controls::LEFT, ui_controls::DOWN, |
| base::BindLambdaForTesting([&]() { |
| send_custom_event(kMouseDownCustomEvent); |
| }))); |
| })) |
| .Build()) |
| // Release the left mouse button. |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kCustomEvent, |
| kMouseDownCustomEvent) |
| .SetElementID(kWebContentsElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| EXPECT_TRUE( |
| ui_controls::SendMouseEventsNotifyWhenDone( |
| ui_controls::LEFT, ui_controls::UP, |
| base::BindLambdaForTesting([&]() { |
| send_custom_event(kMouseUpCustomEvent); |
| }))); |
| })) |
| .Build()) |
| // Once the left mouse button has been released, the text field should |
| // become visible, so wait for it. |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kCustomEvent, |
| kMouseUpCustomEvent) |
| .SetElementID(kWebContentsElementId) |
| .SetStartCallback(base::BindLambdaForTesting( |
| [&](ui::InteractionSequence*, |
| ui::TrackedElement* element) { |
| WebContentsInteractionTestUtil::StateChange change; |
| change.where = kTextQuery; |
| change.event = kTextVisibleCustomEvent; |
| change.test_function = |
| "el => el.getBoundingClientRect().width > 0"; |
| element->AsA<TrackedElementWebContents>() |
| ->owner() |
| ->SendEventOnStateChange(change); |
| })) |
| .Build()) |
| // If the text appears as expected, the test is complete. |
| .AddStep(ui::InteractionSequence::StepBuilder() |
| .SetType(ui::InteractionSequence::StepType::kCustomEvent, |
| kTextVisibleCustomEvent) |
| .SetElementID(kWebContentsElementId) |
| .Build()) |
| .Build(); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, sequence->RunSynchronouslyForTesting()); |
| } |