blob: 7e650e692d9eadfb6d3cca99396289c49b4d0bb3 [file] [log] [blame]
// Copyright 2021 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 "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/test_simple_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/win/windows_version.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/hit_test_region_observer.h"
#include "net/dns/mock_host_resolver.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/views/corewm/tooltip_aura.h"
#include "ui/views/corewm/tooltip_controller.h"
#include "ui/views/corewm/tooltip_controller_test_helper.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
using content::RenderFrameHost;
using content::RenderWidgetHostView;
using content::WebContents;
using views::corewm::TooltipAura;
using views::corewm::TooltipController;
using views::corewm::TooltipStateManager;
using views::corewm::test::TooltipControllerTestHelper;
class TooltipWidgetMonitor {
public:
TooltipWidgetMonitor() {
observer_ = std::make_unique<views::AnyWidgetObserver>(
views::test::AnyWidgetTestPasskey());
observer_->set_shown_callback(base::BindRepeating(
&TooltipWidgetMonitor::OnTooltipShown, base::Unretained(this)));
observer_->set_closing_callback(base::BindRepeating(
&TooltipWidgetMonitor::OnTooltipClosed, base::Unretained(this)));
}
TooltipWidgetMonitor(const TooltipWidgetMonitor&) = delete;
TooltipWidgetMonitor& operator=(const TooltipWidgetMonitor&) = delete;
~TooltipWidgetMonitor() = default;
bool IsWidgetActive() const { return active_widget_; }
void WaitUntilTooltipShown() {
if (!active_widget_) {
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
}
}
void WaitUntilTooltipClosed() {
if (active_widget_) {
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
}
}
void OnTooltipShown(views::Widget* widget) {
if (widget->GetName() == TooltipAura::kWidgetName) {
active_widget_ = widget;
if (run_loop_ && run_loop_->running())
run_loop_->Quit();
}
}
void OnTooltipClosed(views::Widget* widget) {
if (active_widget_ == widget) {
active_widget_ = nullptr;
if (run_loop_ && run_loop_->running())
run_loop_->Quit();
}
}
private:
// This attribute will help us avoid starting the |run_loop_| when the widget
// is already shown. Otherwise, we would be waiting infinitely for an event
// that already occurred.
views::Widget* active_widget_ = nullptr;
std::unique_ptr<views::AnyWidgetObserver> observer_;
std::unique_ptr<base::RunLoop> run_loop_;
}; // class TooltipWidgetMonitor
// Browser tests for tooltips on platforms that use TooltipAura to display the
// tooltip.
class TooltipBrowserTest : public InProcessBrowserTest {
public:
TooltipBrowserTest() = default;
~TooltipBrowserTest() override = default;
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
gfx::NativeWindow root_window =
browser()->window()->GetNativeWindow()->GetRootWindow();
event_generator_ = std::make_unique<ui::test::EventGenerator>(root_window);
helper_ = std::make_unique<TooltipControllerTestHelper>(
static_cast<TooltipController*>(wm::GetTooltipClient(root_window)));
tooltip_monitor_ = std::make_unique<TooltipWidgetMonitor>();
}
content::WebContents* web_contents() { return web_contents_; }
protected:
void NavigateToURL(const std::string& relative_url) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("a.com", relative_url)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
rwhv_ = web_contents_->GetRenderWidgetHostView();
content::WaitForHitTestData(web_contents_->GetMainFrame());
}
void LoadCrossSitePageIntoFrame(const std::string& relative_url,
const std::string& iframe_id) {
// This function makes the iframe in the web page point to a different
// origin, making it an OOPIF.
GURL frame_url(embedded_test_server()->GetURL("b.com", relative_url));
NavigateIframeToURL(web_contents_, iframe_id, frame_url);
// Verify that the child frame is an OOPIF with a different SiteInstance.
RenderFrameHost* child = GetChildRenderFrameHost(0);
EXPECT_NE(web_contents_->GetSiteInstance(), child->GetSiteInstance());
EXPECT_TRUE(child->IsCrossProcessSubframe());
content::WaitForHitTestData(child);
}
RenderFrameHost* GetChildRenderFrameHost(size_t index) {
return ChildFrameAt(web_contents_->GetMainFrame(), index);
}
bool SkipTestForOldWinVersion() const {
#if defined(OS_WIN)
// On older Windows version, tooltips are displayed with TooltipWin instead
// of TooltipAura. For TooltipAura, a tooltip is displayed using a Widget
// and a Label and for TooltipWin, it is displayed using a native win32
// control. Since the observer we use in this class is the
// AnyWidgetObserver, we don't receive any update from non-Widget tooltips.
// This doesn't mean that no tooltip is displayed on older platforms, but
// that we are unable to execute the browser test successfully because the
// tooltip displayed is not displayed using a Widget.
//
// For now, we can simply skip the tests on older platforms, but it might be
// a good idea to eventually implement a custom observer (e.g.,
// TooltipStateObserver) that would work for both TooltipAura and
// TooltipWin, or remove once and for all TooltipWin. For more information
// on why we still need TooltipWin on Win7, see https://crbug.com/1201440.
if (base::win::GetVersion() <= base::win::Version::WIN7)
return true;
#endif // defined(OS_WIN)
return false;
}
gfx::Point WebContentPositionToScreenCoordinate(int x, int y) {
return gfx::Point(x, y) + rwhv_->GetViewBounds().OffsetFromOrigin();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"KeyboardAccessibleTooltip");
scoped_feature_list_.InitWithFeatures(
{features::kKeyboardAccessibleTooltip}, {});
}
ui::test::EventGenerator* event_generator() { return event_generator_.get(); }
TooltipControllerTestHelper* helper() { return helper_.get(); }
TooltipWidgetMonitor* tooltip_monitor() { return tooltip_monitor_.get(); }
private:
std::unique_ptr<ui::test::EventGenerator> event_generator_ = nullptr;
RenderWidgetHostView* rwhv_ = nullptr;
WebContents* web_contents_ = nullptr;
std::unique_ptr<TooltipControllerTestHelper> helper_;
std::unique_ptr<TooltipWidgetMonitor> tooltip_monitor_ = nullptr;
base::test::ScopedFeatureList scoped_feature_list_;
}; // class TooltipBrowserTest
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
ShowTooltipFromWebContentWithCursor) {
if (SkipTestForOldWinVersion())
return;
NavigateToURL("/tooltip.html");
std::u16string expected_text = u"my tooltip";
// Trigger the tooltip from the cursor.
// This will position the cursor right above a button with the title attribute
// set and will show the tooltip.
gfx::Point position = WebContentPositionToScreenCoordinate(10, 10);
event_generator()->MoveMouseTo(position);
tooltip_monitor()->WaitUntilTooltipShown();
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text, helper()->GetTooltipText());
helper()->HideAndReset();
}
#if defined(OS_CHROMEOS) || defined(OS_LINUX)
// https://crbug.com/1212403. Flaky on linux-chromeos-rel and other linux bots.
#define MAYBE_ShowTooltipFromWebContentWithKeyboard \
DISABLED_ShowTooltipFromWebContentWithKeyboard
#else
#define MAYBE_ShowTooltipFromWebContentWithKeyboard \
ShowTooltipFromWebContentWithKeyboard
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
MAYBE_ShowTooltipFromWebContentWithKeyboard) {
if (SkipTestForOldWinVersion())
return;
NavigateToURL("/tooltip.html");
std::u16string expected_text = u"my tooltip";
// Trigger the tooltip from the keyboard with a TAB keypress.
event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
tooltip_monitor()->WaitUntilTooltipShown();
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text, helper()->GetTooltipText());
helper()->HideAndReset();
}
#if defined(OS_CHROMEOS) || defined(OS_WIN)
// https://crbug.com/1212403. Flaky on linux-chromeos-rel.
// https://crbug.com/1241736. Flaky on Win.
#define MAYBE_ShowTooltipFromIFrameWithKeyboard \
DISABLED_ShowTooltipFromIFrameWithKeyboard
#else
#define MAYBE_ShowTooltipFromIFrameWithKeyboard \
ShowTooltipFromIFrameWithKeyboard
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
MAYBE_ShowTooltipFromIFrameWithKeyboard) {
if (SkipTestForOldWinVersion())
return;
// There are two tooltips in this file: one above the iframe and one inside
// the iframe.
NavigateToURL("/tooltip_in_iframe.html");
std::u16string expected_text = u"my tooltip";
// Make the iframe cross-origin.
LoadCrossSitePageIntoFrame("/tooltip.html", "iframe1");
// Move the focus to the button outside of the iframe to get its position.
// We'll use it in the next step to validate that the tooltip associated with
// the button inside of the iframe is positioned, well, inside the iframe.
event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
tooltip_monitor()->WaitUntilTooltipShown();
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text, helper()->GetTooltipText());
gfx::Point first_tooltip_anchor_point = helper()->GetTooltipPosition();
// Now that we have the anchor point of the first tooltip, move the focus to
// the tooltip inside of the iframe.
event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
tooltip_monitor()->WaitUntilTooltipShown();
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text, helper()->GetTooltipText());
// Validate that the tooltip is correctly positioned inside the iframe.
// Because we explicitly removed the iframe's borders and body margins, the
// buttons should be positioned at the exact same x value as the button above,
// and at exactly twice the y value as the one outside the iframe (because the
// tooltip's anchor point is at the bottom-center of the button).
int expected_y = 2 * first_tooltip_anchor_point.y();
EXPECT_EQ(gfx::Point(first_tooltip_anchor_point.x(), expected_y),
helper()->GetTooltipPosition());
helper()->HideAndReset();
}
#if defined(OS_CHROMEOS)
// https://crbug.com/1212403. Flaky on linux-chromeos-rel.
#define MAYBE_HideTooltipOnKeyPress DISABLED_HideTooltipOnKeyPress
#else
#define MAYBE_HideTooltipOnKeyPress HideTooltipOnKeyPress
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest, MAYBE_HideTooltipOnKeyPress) {
if (SkipTestForOldWinVersion())
return;
NavigateToURL("/tooltip.html");
std::u16string expected_text = u"my tooltip";
// First, trigger the tooltip from the cursor.
gfx::Point position = WebContentPositionToScreenCoordinate(10, 10);
event_generator()->MoveMouseTo(position);
tooltip_monitor()->WaitUntilTooltipShown();
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text, helper()->GetTooltipText());
// Second, send a key press event to test whether the tooltip gets hidden.
EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());
event_generator()->PressKey(ui::VKEY_A, ui::EF_NONE);
event_generator()->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
tooltip_monitor()->WaitUntilTooltipClosed();
EXPECT_FALSE(helper()->IsTooltipVisible());
// Try the same thing with a keyboard-triggered tooltip.
// Trigger the tooltip from the keyboard with a TAB keypress.
event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
tooltip_monitor()->WaitUntilTooltipShown();
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text, helper()->GetTooltipText());
// Send a key press event to test whether the tooltip gets hidden.
EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());
event_generator()->PressKey(ui::VKEY_A, ui::EF_NONE);
event_generator()->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
tooltip_monitor()->WaitUntilTooltipClosed();
EXPECT_FALSE(helper()->IsTooltipVisible());
}
#if defined(OS_CHROMEOS)
// https://crbug.com/1212403. Flaky on linux-chromeos-rel.
#define MAYBE_ScriptFocusHidesKeyboardTriggeredTooltip \
DISABLED_ScriptFocusHidesKeyboardTriggeredTooltip
#else
#define MAYBE_ScriptFocusHidesKeyboardTriggeredTooltip \
ScriptFocusHidesKeyboardTriggeredTooltip
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
MAYBE_ScriptFocusHidesKeyboardTriggeredTooltip) {
if (SkipTestForOldWinVersion())
return;
NavigateToURL("/tooltip_two_buttons.html");
std::u16string expected_text_1 = u"my tooltip 1";
std::u16string expected_text_2 = u"my tooltip 2";
// Trigger the tooltip from the keyboard with a TAB keypress.
event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
tooltip_monitor()->WaitUntilTooltipShown();
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text_1, helper()->GetTooltipText());
EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());
// Validate that a blur event on another element than our focused one doesn't
// hide the tooltip.
std::string javascript = "document.getElementById('b2').blur();";
EXPECT_TRUE(content::ExecuteScript(web_contents(), javascript));
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text_1, helper()->GetTooltipText());
EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());
// Validate that a focus on another element will hide the tooltip.
javascript = "document.getElementById('b2').focus();";
EXPECT_TRUE(content::ExecuteScript(web_contents(), javascript));
tooltip_monitor()->WaitUntilTooltipClosed();
EXPECT_FALSE(tooltip_monitor()->IsWidgetActive());
EXPECT_FALSE(helper()->IsTooltipVisible());
// Move the focus again to the first button to test the blur on the focused
// element.
event_generator()->PressKey(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
tooltip_monitor()->WaitUntilTooltipShown();
EXPECT_TRUE(helper()->IsTooltipVisible());
EXPECT_EQ(expected_text_1, helper()->GetTooltipText());
EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());
// Validate that the blur call hides the tooltip.
javascript = "document.getElementById('b1').blur();";
EXPECT_TRUE(content::ExecuteScript(web_contents(), javascript));
EXPECT_FALSE(tooltip_monitor()->IsWidgetActive());
EXPECT_FALSE(helper()->IsTooltipVisible());
}