blob: d0336f1b8ba006a06bb363aa96bd4dea7898a4d4 [file] [log] [blame]
// Copyright 2018 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 <memory>
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_view_views.h"
#include "chrome/browser/ui/views/omnibox/rounded_omnibox_results_frame.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_popup_model.h"
#include "content/public/test/test_utils.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/theme_provider.h"
#include "ui/compositor/layer_animator.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/accessibility/ax_event_manager.h"
#include "ui/views/accessibility/ax_event_observer.h"
#include "ui/views/widget/widget.h"
#if defined(USE_AURA)
#include "ui/aura/window.h"
#endif
namespace {
// A View that positions itself over another View to intercept clicks.
class ClickTrackingOverlayView : public views::View {
public:
explicit ClickTrackingOverlayView(OmniboxResultView* result) {
// |result|'s parent is the OmniboxPopupContentsView, which expects that all
// its children are OmniboxResultViews. So skip over it and add this to the
// OmniboxPopupContentsView's parent.
auto* contents = result->parent();
SetBoundsRect(contents->ConvertRectToParent(result->bounds()));
contents->parent()->AddChildView(this);
}
// views::View:
void OnMouseEvent(ui::MouseEvent* event) override {
last_click_ = event->location();
}
base::Optional<gfx::Point> last_click() const { return last_click_; }
private:
base::Optional<gfx::Point> last_click_;
};
// Helper to wait for theme changes. The wait is triggered when an instance of
// this class goes out of scope.
class ThemeChangeWaiter {
public:
explicit ThemeChangeWaiter(ThemeService* theme_service)
: theme_change_observer_(chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
content::Source<ThemeService>(theme_service)) {}
~ThemeChangeWaiter() {
theme_change_observer_.Wait();
// Theme changes propagate asynchronously in DesktopWindowTreeHostX11::
// FrameTypeChanged(), so ensure all tasks are consumed.
content::RunAllPendingInMessageLoop();
}
private:
content::WindowedNotificationObserver theme_change_observer_;
DISALLOW_COPY_AND_ASSIGN(ThemeChangeWaiter);
};
class TestAXEventObserver : public views::AXEventObserver {
public:
TestAXEventObserver() { views::AXEventManager::Get()->AddObserver(this); }
~TestAXEventObserver() override {
views::AXEventManager::Get()->RemoveObserver(this);
}
// views::AXEventObserver:
void OnViewEvent(views::View* view, ax::mojom::Event event_type) override {
if (!view->GetWidget())
return;
ui::AXNodeData node_data;
view->GetAccessibleNodeData(&node_data);
if (event_type == ax::mojom::Event::kTextChanged &&
node_data.role == ax::mojom::Role::kListBoxOption)
text_changed_on_listboxoption_count_++;
else if (event_type == ax::mojom::Event::kSelectedChildrenChanged)
selected_children_changed_count_++;
}
int text_changed_on_listboxoption_count() {
return text_changed_on_listboxoption_count_;
}
int selected_children_changed_count() {
return selected_children_changed_count_;
}
private:
int text_changed_on_listboxoption_count_ = 0;
int selected_children_changed_count_ = 0;
DISALLOW_COPY_AND_ASSIGN(TestAXEventObserver);
};
} // namespace
class OmniboxPopupContentsViewTest : public InProcessBrowserTest {
public:
OmniboxPopupContentsViewTest() {}
views::Widget* CreatePopupForTestQuery();
views::Widget* GetPopupWidget() { return popup_view()->GetWidget(); }
OmniboxResultView* GetResultViewAt(int index) {
return popup_view()->result_view_at(index);
}
LocationBarView* location_bar() {
auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
return browser_view->toolbar()->location_bar();
}
OmniboxViewViews* omnibox_view() { return location_bar()->omnibox_view(); }
OmniboxEditModel* edit_model() { return omnibox_view()->model(); }
OmniboxPopupModel* popup_model() { return edit_model()->popup_model(); }
OmniboxPopupContentsView* popup_view() {
return static_cast<OmniboxPopupContentsView*>(popup_model()->view());
}
private:
base::test::ScopedFeatureList feature_list_;
DISALLOW_COPY_AND_ASSIGN(OmniboxPopupContentsViewTest);
};
views::Widget* OmniboxPopupContentsViewTest::CreatePopupForTestQuery() {
EXPECT_TRUE(popup_model()->result().empty());
EXPECT_FALSE(popup_view()->IsOpen());
EXPECT_FALSE(GetPopupWidget());
edit_model()->SetUserText(base::ASCIIToUTF16("foo"));
AutocompleteInput input(
base::ASCIIToUTF16("foo"), metrics::OmniboxEventProto::BLANK,
ChromeAutocompleteSchemeClassifier(browser()->profile()));
input.set_want_asynchronous_matches(false);
popup_model()->autocomplete_controller()->Start(input);
EXPECT_FALSE(popup_model()->result().empty());
EXPECT_TRUE(popup_view()->IsOpen());
views::Widget* popup = GetPopupWidget();
EXPECT_TRUE(popup);
return popup;
}
// Tests widget alignment of the different popup types.
IN_PROC_BROWSER_TEST_F(OmniboxPopupContentsViewTest, PopupAlignment) {
views::Widget* popup = CreatePopupForTestQuery();
#if defined(USE_AURA)
popup_view()->UpdatePopupAppearance();
#endif // defined(USE_AURA)
gfx::Rect alignment_rect = location_bar()->GetBoundsInScreen();
alignment_rect.Inset(
-RoundedOmniboxResultsFrame::GetLocationBarAlignmentInsets());
alignment_rect.Inset(-RoundedOmniboxResultsFrame::GetShadowInsets());
// Top, left and right should align. Bottom depends on the results.
gfx::Rect popup_rect = popup->GetRestoredBounds();
EXPECT_EQ(popup_rect.y(), alignment_rect.y());
EXPECT_EQ(popup_rect.x(), alignment_rect.x());
EXPECT_EQ(popup_rect.right(), alignment_rect.right());
}
// Integration test for omnibox popup theming.
IN_PROC_BROWSER_TEST_F(OmniboxPopupContentsViewTest, ThemeIntegration) {
// This test relies on the light/dark variants of the result background to be
// different. But when using the GTK theme on Linux, these colors will be the
// same. Ensure we're not using the system (GTK) theme, which may be
// conditionally enabled depending on the environment.
ThemeService* theme_service =
ThemeServiceFactory::GetForProfile(browser()->profile());
if (!theme_service->UsingDefaultTheme()) {
ThemeChangeWaiter wait(theme_service);
theme_service->UseDefaultTheme();
}
ASSERT_TRUE(theme_service->UsingDefaultTheme());
Browser* regular_browser = browser();
BrowserView* browser_view =
BrowserView::GetBrowserViewForBrowser(regular_browser);
browser_view->GetNativeTheme()->set_use_dark_colors(true);
const ui::ThemeProvider* theme_provider = browser_view->GetThemeProvider();
const SkColor selection_color_dark =
GetOmniboxColor(theme_provider, OmniboxPart::RESULTS_BACKGROUND,
OmniboxPartState::SELECTED);
browser_view->GetNativeTheme()->set_use_dark_colors(false);
const SkColor selection_color_light =
GetOmniboxColor(theme_provider, OmniboxPart::RESULTS_BACKGROUND,
OmniboxPartState::SELECTED);
// Unthemed, non-incognito always has a white background. Exceptions: Inverted
// color themes on Windows and GTK (not tested here).
EXPECT_EQ(SK_ColorWHITE,
GetOmniboxColor(theme_provider, OmniboxPart::RESULTS_BACKGROUND));
auto get_selection_color = [](const Browser* browser) {
return GetOmniboxColor(
BrowserView::GetBrowserViewForBrowser(browser)->GetThemeProvider(),
OmniboxPart::RESULTS_BACKGROUND, OmniboxPartState::SELECTED);
};
// Tests below are mainly interested just whether things change, so ensure
// that can be detected.
EXPECT_NE(selection_color_dark, selection_color_light);
EXPECT_EQ(selection_color_light, get_selection_color(regular_browser));
// Check unthemed incognito windows.
Browser* incognito_browser = CreateIncognitoBrowser();
EXPECT_EQ(selection_color_dark, get_selection_color(incognito_browser));
// Install a theme (in both browsers, since it's the same profile).
extensions::ChromeTestExtensionLoader loader(browser()->profile());
{
ThemeChangeWaiter wait(theme_service);
base::FilePath path = ui_test_utils::GetTestFilePath(
base::FilePath().AppendASCII("extensions"),
base::FilePath().AppendASCII("theme"));
loader.LoadExtension(path);
}
// Check the incognito browser first. Everything should now be light.
EXPECT_EQ(selection_color_light, get_selection_color(incognito_browser));
// Same in the non-incognito browser.
EXPECT_EQ(selection_color_light, get_selection_color(regular_browser));
// Switch to the default theme without installing a custom theme. E.g. this is
// what gets used on KDE or when switching to the "classic" theme in settings.
{
ThemeChangeWaiter wait(theme_service);
theme_service->UseDefaultTheme();
}
EXPECT_EQ(selection_color_light, get_selection_color(regular_browser));
// Check incognito again. It should now use a dark theme, even on Linux.
EXPECT_EQ(selection_color_dark, get_selection_color(incognito_browser));
}
// TODO(tapted): https://crbug.com/905508 Fix and enable on Mac.
#if defined(OS_MACOSX)
#define MAYBE_ClickOmnibox DISABLED_ClickOmnibox
#else
#define MAYBE_ClickOmnibox ClickOmnibox
#endif
// Test that clicks over the omnibox do not hit the popup.
IN_PROC_BROWSER_TEST_F(OmniboxPopupContentsViewTest, MAYBE_ClickOmnibox) {
CreatePopupForTestQuery();
gfx::NativeWindow event_window = browser()->window()->GetNativeWindow();
#if defined(USE_AURA)
event_window = event_window->GetRootWindow();
#endif
ui::test::EventGenerator generator(event_window);
OmniboxResultView* result = GetResultViewAt(0);
ASSERT_TRUE(result);
// Sanity check: ensure the EventGenerator clicks where we think it should
// when clicking on a result (but don't dismiss the popup yet). This will fail
// if the WindowTreeHost and EventGenerator coordinate systems do not align.
{
const gfx::Point expected_point = result->GetLocalBounds().CenterPoint();
EXPECT_NE(gfx::Point(), expected_point);
ClickTrackingOverlayView overlay(result);
generator.MoveMouseTo(result->GetBoundsInScreen().CenterPoint());
generator.ClickLeftButton();
auto click = overlay.last_click();
ASSERT_TRUE(click.has_value());
ASSERT_EQ(expected_point, click.value());
}
// Select the text, so that we can test whether a click is received (which
// should deselect the text);
omnibox_view()->SelectAll(true);
views::Textfield* textfield = omnibox_view();
EXPECT_EQ(base::ASCIIToUTF16("foo"), textfield->GetSelectedText());
generator.MoveMouseTo(location_bar()->GetBoundsInScreen().CenterPoint());
generator.ClickLeftButton();
EXPECT_EQ(base::string16(), textfield->GetSelectedText());
// Clicking the result should dismiss the popup (asynchronously).
generator.MoveMouseTo(result->GetBoundsInScreen().CenterPoint());
ASSERT_TRUE(GetPopupWidget());
EXPECT_FALSE(GetPopupWidget()->IsClosed());
generator.ClickLeftButton();
ASSERT_TRUE(GetPopupWidget());
// Instantly finish all queued animations.
GetPopupWidget()->GetLayer()->GetAnimator()->StopAnimating();
EXPECT_TRUE(GetPopupWidget()->IsClosed());
}
// Check that the location bar background (and the background of the textfield
// it contains) changes when it receives focus, and matches the popup background
// color.
IN_PROC_BROWSER_TEST_F(OmniboxPopupContentsViewTest,
PopupMatchesLocationBarBackground) {
// In dark mode the omnibox focused and unfocused colors are the same, which
// makes this test fail; see comments below.
BrowserView::GetBrowserViewForBrowser(browser())
->GetNativeTheme()
->set_use_dark_colors(false);
// Start with the Omnibox unfocused.
omnibox_view()->GetFocusManager()->ClearFocus();
const SkColor color_before_focus = location_bar()->background()->get_color();
EXPECT_EQ(color_before_focus, omnibox_view()->GetBackgroundColor());
// Give the Omnibox focus and get its focused color.
omnibox_view()->RequestFocus();
const SkColor color_after_focus = location_bar()->background()->get_color();
// Sanity check that the colors are different, otherwise this test will not be
// testing anything useful. It is possible that a particular theme could
// configure these colors to be the same. In that case, this test should be
// updated to detect that, or switch to a theme where they are different.
EXPECT_NE(color_before_focus, color_after_focus);
EXPECT_EQ(color_after_focus, omnibox_view()->GetBackgroundColor());
// The background is hosted in the view that contains the results area.
CreatePopupForTestQuery();
views::View* background_host = popup_view()->parent();
EXPECT_EQ(color_after_focus, background_host->background()->get_color());
// Blurring the Omnibox should restore the original colors.
omnibox_view()->GetFocusManager()->ClearFocus();
EXPECT_EQ(color_before_focus, location_bar()->background()->get_color());
EXPECT_EQ(color_before_focus, omnibox_view()->GetBackgroundColor());
}
IN_PROC_BROWSER_TEST_F(OmniboxPopupContentsViewTest,
EmitTextChangedAccessibilityEvent) {
// Creation and population of the popup should not result in a text/name
// change accessibility event.
TestAXEventObserver observer;
CreatePopupForTestQuery();
ACMatches matches;
AutocompleteMatch match(nullptr, 500, false,
AutocompleteMatchType::HISTORY_TITLE);
AutocompleteController* controller = popup_model()->autocomplete_controller();
match.contents = base::ASCIIToUTF16("https://foobar.com");
matches.push_back(match);
match.contents = base::ASCIIToUTF16("https://foobarbaz.com");
matches.push_back(match);
controller->result_.AppendMatches(controller->input_, matches);
popup_view()->UpdatePopupAppearance();
EXPECT_EQ(observer.text_changed_on_listboxoption_count(), 0);
// Changing the user text while in the input rather than the list should not
// result in a text/name change accessibility event.
edit_model()->SetUserText(base::ASCIIToUTF16("bar"));
edit_model()->StartAutocomplete(false, false);
popup_view()->UpdatePopupAppearance();
EXPECT_EQ(observer.text_changed_on_listboxoption_count(), 0);
// Each time the selection changes, we should have a text/name change event.
// This makes it possible for screen readers to have the updated match content
// when they are notified the selection changed.
popup_view()->model()->SetSelectedLine(1, false, false);
EXPECT_EQ(observer.text_changed_on_listboxoption_count(), 1);
popup_view()->model()->SetSelectedLine(2, false, false);
EXPECT_EQ(observer.text_changed_on_listboxoption_count(), 2);
}
IN_PROC_BROWSER_TEST_F(OmniboxPopupContentsViewTest,
EmitSelectedChildrenChangedAccessibilityEvent) {
// Create a popup for the matches.
GetPopupWidget();
edit_model()->SetUserText(base::ASCIIToUTF16("foo"));
AutocompleteInput input(
base::ASCIIToUTF16("foo"), metrics::OmniboxEventProto::BLANK,
ChromeAutocompleteSchemeClassifier(browser()->profile()));
input.set_want_asynchronous_matches(false);
popup_model()->autocomplete_controller()->Start(input);
// Create a match to populate the autocomplete.
base::string16 match_url = base::ASCIIToUTF16("https://foobar.com");
AutocompleteMatch match(nullptr, 500, false,
AutocompleteMatchType::HISTORY_TITLE);
match.contents = match_url;
match.contents_class.push_back(
ACMatchClassification(0, ACMatchClassification::URL));
match.destination_url = GURL(match_url);
match.description = base::ASCIIToUTF16("Foobar");
match.allowed_to_be_default_match = true;
AutocompleteController* autocomplete_controller =
popup_model()->autocomplete_controller();
AutocompleteResult& results = autocomplete_controller->result_;
ACMatches matches;
matches.push_back(match);
results.AppendMatches(input, matches);
results.SortAndCull(input, nullptr);
autocomplete_controller->NotifyChanged(true);
// Lets check that arrowing up and down emits the event.
TestAXEventObserver observer;
EXPECT_EQ(observer.selected_children_changed_count(), 0);
// This is equiverlent of the user arrowing down in the omnibox.
popup_view()->model()->SetSelectedLine(1, false, false);
EXPECT_EQ(observer.selected_children_changed_count(), 1);
}