| // Copyright (c) 2015 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 "chrome/browser/ui/views/omnibox/omnibox_view_views.h" |
| |
| #include <stddef.h> |
| |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/macros.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/autocomplete/autocomplete_classifier_factory.h" |
| #include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h" |
| #include "chrome/browser/command_updater.h" |
| #include "chrome/browser/command_updater_impl.h" |
| #include "chrome/browser/search_engines/template_url_service_factory_test_util.h" |
| #include "chrome/browser/ui/omnibox/chrome_omnibox_client.h" |
| #include "chrome/browser/ui/omnibox/chrome_omnibox_edit_controller.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chrome/test/views/chrome_views_test_base.h" |
| #include "components/omnibox/browser/omnibox_edit_model.h" |
| #include "components/omnibox/browser/omnibox_field_trial.h" |
| #include "components/omnibox/browser/test_location_bar_model.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/metrics_proto/omnibox_event.pb.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/base/ime/text_edit_commands.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/render_text.h" |
| #include "ui/gfx/render_text_test_api.h" |
| #include "ui/views/controls/textfield/textfield_test_api.h" |
| |
| #if defined(OS_CHROMEOS) |
| #include "chrome/browser/chromeos/input_method/input_method_configuration.h" |
| #include "chrome/browser/chromeos/input_method/mock_input_method_manager_impl.h" |
| #endif |
| |
| using gfx::Range; |
| using metrics::OmniboxEventProto; |
| |
| namespace { |
| |
| // TestingOmniboxView --------------------------------------------------------- |
| |
| class TestingOmniboxView : public OmniboxViewViews { |
| public: |
| enum BaseTextEmphasis { |
| DEEMPHASIZED, |
| EMPHASIZED, |
| UNSET, |
| }; |
| |
| TestingOmniboxView(OmniboxEditController* controller, |
| std::unique_ptr<OmniboxClient> client); |
| |
| static BaseTextEmphasis to_base_text_emphasis(bool emphasize) { |
| return emphasize ? EMPHASIZED : DEEMPHASIZED; |
| } |
| |
| using views::Textfield::GetRenderText; |
| |
| void ResetEmphasisTestState(); |
| |
| void CheckUpdatePopupCallInfo(size_t call_count, |
| const base::string16& text, |
| const Range& selection_range); |
| |
| void CheckUpdatePopupNotCalled(); |
| |
| Range scheme_range() const { return scheme_range_; } |
| Range emphasis_range() const { return emphasis_range_; } |
| BaseTextEmphasis base_text_emphasis() const { return base_text_emphasis_; } |
| |
| // OmniboxViewViews: |
| void EmphasizeURLComponents() override; |
| void GetAccessibleNodeData(ui::AXNodeData* node_data) override {} |
| using OmniboxView::IsSelectAll; |
| |
| private: |
| // OmniboxViewViews: |
| // There is no popup and it doesn't actually matter whether we change the |
| // visual style of the text, so these methods are all overridden merely to |
| // capture relevant state at the time of the call, to be checked by test code. |
| void UpdatePopup() override; |
| void SetEmphasis(bool emphasize, const Range& range) override; |
| void UpdateSchemeStyle(const Range& range) override; |
| |
| size_t update_popup_call_count_ = 0; |
| base::string16 update_popup_text_; |
| Range update_popup_selection_range_; |
| |
| // Range of the last scheme logged by UpdateSchemeStyle(). |
| Range scheme_range_; |
| |
| // Range of the last text emphasized by SetEmphasis(). |
| Range emphasis_range_; |
| |
| // SetEmphasis() logs whether the base color of the text is emphasized. |
| BaseTextEmphasis base_text_emphasis_ = UNSET; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestingOmniboxView); |
| }; |
| |
| TestingOmniboxView::TestingOmniboxView(OmniboxEditController* controller, |
| std::unique_ptr<OmniboxClient> client) |
| : OmniboxViewViews(controller, |
| std::move(client), |
| false, |
| nullptr, |
| gfx::FontList()) {} |
| |
| void TestingOmniboxView::ResetEmphasisTestState() { |
| base_text_emphasis_ = UNSET; |
| emphasis_range_ = Range::InvalidRange(); |
| scheme_range_ = Range::InvalidRange(); |
| } |
| |
| void TestingOmniboxView::CheckUpdatePopupCallInfo( |
| size_t call_count, |
| const base::string16& text, |
| const Range& selection_range) { |
| EXPECT_EQ(call_count, update_popup_call_count_); |
| EXPECT_EQ(text, update_popup_text_); |
| EXPECT_EQ(selection_range, update_popup_selection_range_); |
| } |
| |
| void TestingOmniboxView::CheckUpdatePopupNotCalled() { |
| EXPECT_EQ(update_popup_call_count_, 0U); |
| } |
| |
| void TestingOmniboxView::EmphasizeURLComponents() { |
| UpdateTextStyle(text(), model()->CurrentTextIsURL(), |
| model()->client()->GetSchemeClassifier()); |
| } |
| |
| void TestingOmniboxView::UpdatePopup() { |
| ++update_popup_call_count_; |
| update_popup_text_ = text(); |
| update_popup_selection_range_ = GetSelectedRange(); |
| } |
| |
| void TestingOmniboxView::SetEmphasis(bool emphasize, const Range& range) { |
| if (range == Range::InvalidRange()) { |
| base_text_emphasis_ = to_base_text_emphasis(emphasize); |
| return; |
| } |
| |
| EXPECT_TRUE(emphasize); |
| emphasis_range_ = range; |
| } |
| |
| void TestingOmniboxView::UpdateSchemeStyle(const Range& range) { |
| scheme_range_ = range; |
| } |
| |
| // TestingOmniboxEditController ----------------------------------------------- |
| |
| class TestingOmniboxEditController : public ChromeOmniboxEditController { |
| public: |
| TestingOmniboxEditController(CommandUpdater* command_updater, |
| LocationBarModel* location_bar_model) |
| : ChromeOmniboxEditController(command_updater), |
| location_bar_model_(location_bar_model) {} |
| |
| private: |
| // ChromeOmniboxEditController: |
| LocationBarModel* GetLocationBarModel() override { |
| return location_bar_model_; |
| } |
| const LocationBarModel* GetLocationBarModel() const override { |
| return location_bar_model_; |
| } |
| |
| LocationBarModel* location_bar_model_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestingOmniboxEditController); |
| }; |
| |
| } // namespace |
| |
| // OmniboxViewViewsTest ------------------------------------------------------- |
| |
| // Base class that ensures ScopedFeatureList is initialized first. |
| class OmniboxViewViewsTestBase : public ChromeViewsTestBase { |
| public: |
| explicit OmniboxViewViewsTestBase( |
| const std::vector<base::Feature>& enabled_features) { |
| scoped_feature_list_.InitWithFeatures(enabled_features, {}); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| class OmniboxViewViewsTest : public OmniboxViewViewsTestBase { |
| public: |
| explicit OmniboxViewViewsTest( |
| const std::vector<base::Feature>& enabled_features); |
| |
| OmniboxViewViewsTest() : OmniboxViewViewsTest(std::vector<base::Feature>()) {} |
| |
| TestLocationBarModel* location_bar_model() { return &location_bar_model_; } |
| TestingOmniboxView* omnibox_view() const { return omnibox_view_; } |
| views::Textfield* omnibox_textfield() const { return omnibox_view(); } |
| ui::TextEditCommand scheduled_text_edit_command() const { |
| return test_api_->scheduled_text_edit_command(); |
| } |
| |
| // Sets |new_text| as the omnibox text, and emphasizes it appropriately. If |
| // |accept_input| is true, pretends that the user has accepted this input |
| // (i.e. it's been navigated to). |
| void SetAndEmphasizeText(const std::string& new_text, bool accept_input); |
| |
| bool IsCursorEnabled() const { |
| return test_api_->GetRenderText()->cursor_enabled(); |
| } |
| |
| protected: |
| // testing::Test: |
| void SetUp() override; |
| void TearDown() override; |
| |
| ui::MouseEvent CreateEvent(ui::EventType type, int flags) { |
| return ui::MouseEvent(type, gfx::Point(0, 0), gfx::Point(), |
| ui::EventTimeForNow(), flags, 0); |
| } |
| |
| private: |
| TestingProfile profile_; |
| TemplateURLServiceFactoryTestUtil util_; |
| CommandUpdaterImpl command_updater_; |
| TestLocationBarModel location_bar_model_; |
| TestingOmniboxEditController omnibox_edit_controller_; |
| |
| std::unique_ptr<views::Widget> widget_; |
| |
| // Owned by |widget_|. |
| TestingOmniboxView* omnibox_view_; |
| |
| std::unique_ptr<views::TextfieldTestApi> test_api_; |
| |
| DISALLOW_COPY_AND_ASSIGN(OmniboxViewViewsTest); |
| }; |
| |
| OmniboxViewViewsTest::OmniboxViewViewsTest( |
| const std::vector<base::Feature>& enabled_features) |
| : OmniboxViewViewsTestBase(enabled_features), |
| util_(&profile_), |
| command_updater_(nullptr), |
| omnibox_edit_controller_(&command_updater_, &location_bar_model_) {} |
| |
| void OmniboxViewViewsTest::SetAndEmphasizeText(const std::string& new_text, |
| bool accept_input) { |
| omnibox_view()->ResetEmphasisTestState(); |
| omnibox_view()->SetUserText(base::ASCIIToUTF16(new_text)); |
| if (accept_input) { |
| // We don't need to actually navigate in this case (and doing so in a test |
| // would be difficult); it's sufficient to mark input as "no longer in |
| // progress", and the edit model will assume the current text is a URL. |
| omnibox_view()->model()->SetInputInProgress(false); |
| } |
| omnibox_view()->EmphasizeURLComponents(); |
| } |
| |
| void OmniboxViewViewsTest::SetUp() { |
| ChromeViewsTestBase::SetUp(); |
| |
| // We need a widget so OmniboxView can be correctly focused and unfocused. |
| widget_ = std::make_unique<views::Widget>(); |
| views::Widget::InitParams params = |
| CreateParams(views::Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.bounds = gfx::Rect(0, 0, 400, 80); |
| widget_->Init(params); |
| widget_->Show(); |
| |
| #if defined(OS_CHROMEOS) |
| chromeos::input_method::InitializeForTesting( |
| new chromeos::input_method::MockInputMethodManagerImpl); |
| #endif |
| AutocompleteClassifierFactory::GetInstance()->SetTestingFactoryAndUse( |
| &profile_, |
| base::BindRepeating(&AutocompleteClassifierFactory::BuildInstanceFor)); |
| omnibox_view_ = new TestingOmniboxView( |
| &omnibox_edit_controller_, std::make_unique<ChromeOmniboxClient>( |
| &omnibox_edit_controller_, &profile_)); |
| test_api_ = std::make_unique<views::TextfieldTestApi>(omnibox_view_); |
| omnibox_view_->Init(); |
| |
| widget_->SetContentsView(omnibox_view_); |
| } |
| |
| void OmniboxViewViewsTest::TearDown() { |
| // Clean ourselves up as the text input client. |
| if (omnibox_view_->GetInputMethod()) |
| omnibox_view_->GetInputMethod()->DetachTextInputClient(omnibox_view_); |
| |
| widget_.reset(); |
| |
| #if defined(OS_CHROMEOS) |
| chromeos::input_method::Shutdown(); |
| #endif |
| ChromeViewsTestBase::TearDown(); |
| } |
| |
| // Actual tests --------------------------------------------------------------- |
| |
| // Checks that a single change of the text in the omnibox invokes |
| // only one call to OmniboxViewViews::UpdatePopup(). |
| TEST_F(OmniboxViewViewsTest, UpdatePopupCall) { |
| ui::KeyEvent char_event(ui::ET_KEY_PRESSED, ui::VKEY_A, ui::DomCode::US_A, 0, |
| ui::DomKey::FromCharacter('a'), |
| ui::EventTimeForNow()); |
| omnibox_textfield()->InsertChar(char_event); |
| omnibox_view()->CheckUpdatePopupCallInfo(1, base::ASCIIToUTF16("a"), |
| Range(1)); |
| |
| char_event = |
| ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_B, ui::DomCode::US_B, 0, |
| ui::DomKey::FromCharacter('b'), ui::EventTimeForNow()); |
| omnibox_textfield()->InsertChar(char_event); |
| omnibox_view()->CheckUpdatePopupCallInfo(2, base::ASCIIToUTF16("ab"), |
| Range(2)); |
| |
| ui::KeyEvent pressed(ui::ET_KEY_PRESSED, ui::VKEY_BACK, 0); |
| omnibox_textfield()->OnKeyEvent(&pressed); |
| omnibox_view()->CheckUpdatePopupCallInfo(3, base::ASCIIToUTF16("a"), |
| Range(1)); |
| } |
| |
| // Test that text cursor is shown in the omnibox after entering any single |
| // character in NTP 'Search box'. Test for crbug.com/698172. |
| TEST_F(OmniboxViewViewsTest, EditTextfield) { |
| omnibox_textfield()->SetCursorEnabled(false); |
| ui::KeyEvent char_event(ui::ET_KEY_PRESSED, ui::VKEY_A, ui::DomCode::US_A, 0, |
| ui::DomKey::FromCharacter('a'), |
| ui::EventTimeForNow()); |
| omnibox_textfield()->InsertChar(char_event); |
| EXPECT_TRUE(IsCursorEnabled()); |
| } |
| |
| // Test that the scheduled text edit command is cleared when Textfield receives |
| // a key press event. This ensures that the scheduled text edit command property |
| // is always in the correct state. Test for http://crbug.com/613948. |
| TEST_F(OmniboxViewViewsTest, ScheduledTextEditCommand) { |
| omnibox_textfield()->SetTextEditCommandForNextKeyEvent( |
| ui::TextEditCommand::MOVE_UP); |
| EXPECT_EQ(ui::TextEditCommand::MOVE_UP, scheduled_text_edit_command()); |
| |
| ui::KeyEvent up_pressed(ui::ET_KEY_PRESSED, ui::VKEY_UP, 0); |
| omnibox_textfield()->OnKeyEvent(&up_pressed); |
| EXPECT_EQ(ui::TextEditCommand::INVALID_COMMAND, |
| scheduled_text_edit_command()); |
| } |
| |
| // Test that Shift+Up and Shift+Down are not captured and let selection mode |
| // take over. Test for crbug.com/863543 and crbug.com/892216. |
| TEST_F(OmniboxViewViewsTest, SelectWithShift_863543) { |
| location_bar_model()->set_url(GURL("http://www.example.com/?query=1")); |
| const base::string16 text = |
| base::ASCIIToUTF16("http://www.example.com/?query=1"); |
| static_cast<OmniboxView*>(omnibox_view()) |
| ->SetWindowTextAndCaretPos(text, 23U, false, false); |
| |
| ui::KeyEvent shift_up_pressed(ui::ET_KEY_PRESSED, ui::VKEY_UP, |
| ui::EF_SHIFT_DOWN); |
| omnibox_textfield()->OnKeyEvent(&shift_up_pressed); |
| |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(23U, start); |
| EXPECT_EQ(0U, end); |
| omnibox_view()->CheckUpdatePopupNotCalled(); |
| |
| static_cast<OmniboxView*>(omnibox_view()) |
| ->SetWindowTextAndCaretPos(text, 18U, false, false); |
| |
| ui::KeyEvent shift_down_pressed(ui::ET_KEY_PRESSED, ui::VKEY_DOWN, |
| ui::EF_SHIFT_DOWN); |
| omnibox_textfield()->OnKeyEvent(&shift_down_pressed); |
| |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(18U, start); |
| EXPECT_EQ(31U, end); |
| omnibox_view()->CheckUpdatePopupNotCalled(); |
| } |
| |
| TEST_F(OmniboxViewViewsTest, OnBlur) { |
| // Make the Omnibox very narrow (so it couldn't fit the whole string). |
| int kOmniboxWidth = 60; |
| gfx::RenderText* render_text = omnibox_view()->GetRenderText(); |
| render_text->SetDisplayRect(gfx::Rect(0, 0, kOmniboxWidth, 10)); |
| render_text->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| |
| // (In this example, uppercase Latin letters represent Hebrew letters.) |
| // The string |kContentsRtl| is equivalent to: |
| // RA.QWM/0123/abcd |
| // This is displayed as: |
| // 0123/MWQ.AR/abcd |
| // Enter focused mode, where the text should *not* be elided, and we expect |
| // SetWindowTextAndCaretPos to scroll such that the start of the string is |
| // on-screen. Because the domain is RTL, this scrolls to an offset greater |
| // than 0. |
| omnibox_view()->OnFocus(); |
| const base::string16 kContentsRtl = |
| base::WideToUTF16(L"\x05e8\x05e2.\x05e7\x05d5\x05dd/0123/abcd"); |
| static_cast<OmniboxView*>(omnibox_view()) |
| ->SetWindowTextAndCaretPos(kContentsRtl, 0, false, false); |
| EXPECT_EQ(gfx::NO_ELIDE, render_text->elide_behavior()); |
| // NOTE: Technically (depending on the font), this expectation could fail if |
| // the entire domain fits in 60 pixels. However, 60px is so small it should |
| // never happen with any font. |
| EXPECT_GT(0, render_text->GetUpdatedDisplayOffset().x()); |
| omnibox_view()->SelectAll(false); |
| EXPECT_TRUE(omnibox_view()->IsSelectAll()); |
| |
| // Now enter blurred mode, where the text should be elided to 60px. This means |
| // the string itself is truncated. Scrolling would therefore mean the text is |
| // off-screen. Ensure that the horizontal scrolling has been reset to 0. |
| omnibox_view()->OnBlur(); |
| EXPECT_EQ(gfx::ELIDE_TAIL, render_text->elide_behavior()); |
| EXPECT_EQ(0, render_text->GetUpdatedDisplayOffset().x()); |
| EXPECT_FALSE(omnibox_view()->IsSelectAll()); |
| } |
| |
| TEST_F(OmniboxViewViewsTest, Emphasis) { |
| constexpr struct { |
| const char* input; |
| bool expected_base_text_emphasized; |
| Range expected_emphasis_range; |
| Range expected_scheme_range; |
| } test_cases[] = { |
| {"data:text/html,Hello%20World", false, Range(0, 4), Range(0, 4)}, |
| {"http://www.example.com/path/file.htm", false, Range(7, 22), |
| Range(0, 4)}, |
| {"https://www.example.com/path/file.htm", false, Range(8, 23), |
| Range(0, 5)}, |
| {"chrome-extension://ldfbacdbackkjhclmhnjabngnppnkagl", false, |
| Range::InvalidRange(), Range(0, 16)}, |
| {"nosuchscheme://opaque/string", true, Range::InvalidRange(), |
| Range(0, 12)}, |
| {"nosuchscheme:opaquestring", true, Range::InvalidRange(), Range(0, 12)}, |
| {"host.com/path/file", false, Range(0, 8), Range::InvalidRange()}, |
| {"This is plain text", true, Range::InvalidRange(), |
| Range::InvalidRange()}, |
| }; |
| |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(test_case.input); |
| |
| SetAndEmphasizeText(test_case.input, false); |
| EXPECT_EQ(TestingOmniboxView::to_base_text_emphasis( |
| test_case.expected_base_text_emphasized), |
| omnibox_view()->base_text_emphasis()); |
| EXPECT_EQ(test_case.expected_emphasis_range, |
| omnibox_view()->emphasis_range()); |
| EXPECT_FALSE(omnibox_view()->scheme_range().IsValid()); |
| |
| if (test_case.expected_scheme_range.IsValid()) { |
| SetAndEmphasizeText(test_case.input, true); |
| EXPECT_EQ(TestingOmniboxView::to_base_text_emphasis( |
| test_case.expected_base_text_emphasized), |
| omnibox_view()->base_text_emphasis()); |
| EXPECT_EQ(test_case.expected_emphasis_range, |
| omnibox_view()->emphasis_range()); |
| EXPECT_EQ(test_case.expected_scheme_range, |
| omnibox_view()->scheme_range()); |
| } |
| } |
| } |
| |
| TEST_F(OmniboxViewViewsTest, RevertOnBlur) { |
| location_bar_model()->set_url(GURL("https://permanent-text.com/")); |
| omnibox_view()->model()->ResetDisplayTexts(); |
| omnibox_view()->RevertAll(); |
| |
| EXPECT_EQ(base::ASCIIToUTF16("https://permanent-text.com/"), |
| omnibox_view()->text()); |
| EXPECT_FALSE(omnibox_view()->model()->user_input_in_progress()); |
| |
| omnibox_view()->SetUserText(base::ASCIIToUTF16("user text")); |
| |
| EXPECT_EQ(base::ASCIIToUTF16("user text"), omnibox_view()->text()); |
| EXPECT_TRUE(omnibox_view()->model()->user_input_in_progress()); |
| |
| // Expect that on blur, if the text has been edited, stay in user input mode. |
| omnibox_textfield()->OnBlur(); |
| EXPECT_EQ(base::ASCIIToUTF16("user text"), omnibox_view()->text()); |
| EXPECT_TRUE(omnibox_view()->model()->user_input_in_progress()); |
| |
| // Expect that on blur, if the text is the same as the |
| // https://permanent-text.com, exit user input mode. |
| omnibox_view()->SetUserText( |
| base::ASCIIToUTF16("https://permanent-text.com/")); |
| EXPECT_TRUE(omnibox_view()->model()->user_input_in_progress()); |
| omnibox_textfield()->OnBlur(); |
| EXPECT_EQ(base::ASCIIToUTF16("https://permanent-text.com/"), |
| omnibox_view()->text()); |
| EXPECT_FALSE(omnibox_view()->model()->user_input_in_progress()); |
| } |
| |
| TEST_F(OmniboxViewViewsTest, BackspaceExitsKeywordMode) { |
| omnibox_view()->SetUserText(base::UTF8ToUTF16("user text")); |
| omnibox_view()->model()->EnterKeywordModeForDefaultSearchProvider( |
| OmniboxEventProto::KEYBOARD_SHORTCUT); |
| |
| ASSERT_EQ(base::UTF8ToUTF16("user text"), omnibox_view()->GetText()); |
| ASSERT_TRUE(omnibox_view()->IsSelectAll()); |
| ASSERT_FALSE(omnibox_view()->model()->keyword().empty()); |
| |
| // First backspace should clear the user text but not exit keyword mode. |
| ui::KeyEvent backspace(ui::ET_KEY_PRESSED, ui::VKEY_BACK, 0); |
| omnibox_textfield()->OnKeyEvent(&backspace); |
| EXPECT_TRUE(omnibox_view()->GetText().empty()); |
| EXPECT_FALSE(omnibox_view()->model()->keyword().empty()); |
| |
| // Second backspace should exit keyword mode. |
| omnibox_textfield()->OnKeyEvent(&backspace); |
| EXPECT_TRUE(omnibox_view()->GetText().empty()); |
| EXPECT_TRUE(omnibox_view()->model()->keyword().empty()); |
| } |
| |
| class OmniboxViewViewsSteadyStateElisionsTest : public OmniboxViewViewsTest { |
| public: |
| OmniboxViewViewsSteadyStateElisionsTest() |
| : OmniboxViewViewsTest({ |
| omnibox::kHideSteadyStateUrlScheme, |
| omnibox::kHideSteadyStateUrlTrivialSubdomains, |
| }) {} |
| |
| protected: |
| explicit OmniboxViewViewsSteadyStateElisionsTest( |
| const std::vector<base::Feature>& enabled_features) |
| : OmniboxViewViewsTest(enabled_features) {} |
| |
| const int kCharacterWidth = 10; |
| const GURL kFullUrl = GURL("https://www.example.com/"); |
| |
| void SetUp() override { |
| OmniboxViewViewsTest::SetUp(); |
| |
| // Advance 5 seconds from epoch so the time is not considered null. |
| clock_.Advance(base::TimeDelta::FromSeconds(5)); |
| ui::SetEventTickClockForTesting(&clock_); |
| |
| location_bar_model()->set_url(kFullUrl); |
| location_bar_model()->set_url_for_display( |
| base::ASCIIToUTF16("example.com")); |
| |
| gfx::test::RenderTextTestApi render_text_test_api( |
| omnibox_view()->GetRenderText()); |
| render_text_test_api.SetGlyphWidth(kCharacterWidth); |
| |
| omnibox_view()->model()->ResetDisplayTexts(); |
| omnibox_view()->RevertAll(); |
| } |
| |
| void TearDown() override { |
| ui::SetEventTickClockForTesting(nullptr); |
| OmniboxViewViewsTest::TearDown(); |
| } |
| |
| void BlurOmnibox() { |
| ASSERT_TRUE(omnibox_view()->HasFocus()); |
| omnibox_view()->GetFocusManager()->ClearFocus(); |
| ASSERT_FALSE(omnibox_view()->HasFocus()); |
| } |
| |
| void ExpectFullUrlDisplayed() { |
| EXPECT_EQ(base::UTF8ToUTF16(kFullUrl.spec()), omnibox_view()->text()); |
| EXPECT_TRUE(omnibox_view()->model()->user_input_in_progress()); |
| |
| // We test the user text stored in the model has been updated as well. The |
| // model user text is used to populate the text in the Omnibox after some |
| // state transitions, such as the ZeroSuggest popup opening. |
| EXPECT_EQ(base::UTF8ToUTF16(kFullUrl.spec()), |
| omnibox_view()->model()->GetUserTextForTesting()); |
| } |
| |
| bool IsElidedUrlDisplayed() { |
| return omnibox_view()->text() == base::ASCIIToUTF16("example.com") && |
| !omnibox_view()->model()->user_input_in_progress(); |
| } |
| |
| ui::MouseEvent CreateMouseEvent(ui::EventType type, const gfx::Point& point) { |
| return ui::MouseEvent(type, point, gfx::Point(), ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); |
| } |
| |
| // Gets a point at |x_offset| from the beginning of the RenderText. |
| gfx::Point GetPointInTextAtXOffset(int x_offset) { |
| gfx::Rect bounds = omnibox_view()->GetRenderText()->display_rect(); |
| return gfx::Point(bounds.x() + x_offset, bounds.y() + bounds.height() / 2); |
| } |
| |
| // Sends a mouse down and mouse up event at |x_offset| pixels from the |
| // beginning of the RenderText. |
| void SendMouseClick(int x_offset) { |
| gfx::Point point = GetPointInTextAtXOffset(x_offset); |
| omnibox_view()->OnMousePressed( |
| CreateMouseEvent(ui::ET_MOUSE_PRESSED, point)); |
| omnibox_view()->OnMouseReleased( |
| CreateMouseEvent(ui::ET_MOUSE_RELEASED, point)); |
| } |
| |
| // Used to access members that are marked private in views::TextField. |
| views::View* omnibox_textfield_view() { return omnibox_view(); } |
| base::SimpleTestTickClock* clock() { return &clock_; } |
| |
| private: |
| base::SimpleTestTickClock clock_; |
| }; |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, UrlStartsInElidedState) { |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, UnelideOnArrowKey) { |
| SendMouseClick(0); |
| |
| // Right key should unelide and move the cursor to the end. |
| omnibox_textfield_view()->OnKeyPressed( |
| ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_RIGHT, 0)); |
| ExpectFullUrlDisplayed(); |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(23U, start); |
| EXPECT_EQ(23U, end); |
| |
| // Blur to restore the elided URL, then click on the Omnibox again to refocus. |
| BlurOmnibox(); |
| SendMouseClick(0); |
| |
| // Left key should unelide and move the cursor to the beginning of the elided |
| // part. |
| omnibox_textfield_view()->OnKeyPressed( |
| ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_LEFT, 0)); |
| ExpectFullUrlDisplayed(); |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(12U, start); |
| EXPECT_EQ(12U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, UnelideOnHomeKey) { |
| SendMouseClick(0); |
| |
| // Home key should unelide and move the cursor to the beginning of the full |
| // unelided URL. |
| omnibox_textfield_view()->OnKeyPressed( |
| ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_HOME, 0)); |
| ExpectFullUrlDisplayed(); |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(0U, start); |
| EXPECT_EQ(0U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, GestureTaps) { |
| ui::GestureEvent tap_down(0, 0, 0, ui::EventTimeForNow(), |
| ui::GestureEventDetails(ui::ET_GESTURE_TAP_DOWN)); |
| omnibox_textfield_view()->OnGestureEvent(&tap_down); |
| |
| // Select all on first tap. |
| ui::GestureEventDetails tap_details(ui::ET_GESTURE_TAP); |
| tap_details.set_tap_count(1); |
| ui::GestureEvent tap(0, 0, 0, ui::EventTimeForNow(), tap_details); |
| omnibox_textfield_view()->OnGestureEvent(&tap); |
| |
| EXPECT_TRUE(omnibox_view()->IsSelectAll()); |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| |
| // Unelide on second tap (cursor placement). |
| omnibox_textfield_view()->OnGestureEvent(&tap); |
| ExpectFullUrlDisplayed(); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, FirstMouseClickFocusesOnly) { |
| EXPECT_FALSE(omnibox_view()->IsSelectAll()); |
| |
| SendMouseClick(0); |
| |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| EXPECT_TRUE(omnibox_view()->IsSelectAll()); |
| EXPECT_TRUE(omnibox_view()->HasFocus()); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, NegligibleDragKeepsElisions) { |
| gfx::Point click_point = GetPointInTextAtXOffset(2 * kCharacterWidth); |
| omnibox_view()->OnMousePressed( |
| CreateMouseEvent(ui::ET_MOUSE_PRESSED, click_point)); |
| |
| // Offset the drag and release point by an insignificant 2 px. |
| gfx::Point drag_point = click_point; |
| drag_point.Offset(2, 0); |
| omnibox_view()->OnMouseDragged( |
| CreateMouseEvent(ui::ET_MOUSE_DRAGGED, drag_point)); |
| omnibox_view()->OnMouseReleased( |
| CreateMouseEvent(ui::ET_MOUSE_RELEASED, drag_point)); |
| |
| // Expect that after a negligible drag and release, everything is selected. |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| EXPECT_TRUE(omnibox_view()->IsSelectAll()); |
| EXPECT_TRUE(omnibox_view()->HasFocus()); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, CaretPlacementByMouse) { |
| SendMouseClick(0); |
| |
| // Advance the clock 5 seconds so the second click is not interpreted as a |
| // double click. |
| clock()->Advance(base::TimeDelta::FromSeconds(5)); |
| |
| // Second click should unelide only on mouse release. |
| omnibox_view()->OnMousePressed(CreateMouseEvent( |
| ui::ET_MOUSE_PRESSED, GetPointInTextAtXOffset(2 * kCharacterWidth))); |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| omnibox_view()->OnMouseReleased(CreateMouseEvent( |
| ui::ET_MOUSE_RELEASED, GetPointInTextAtXOffset(2 * kCharacterWidth))); |
| ExpectFullUrlDisplayed(); |
| |
| // Verify the cursor position is https://www.ex|ample.com. It should be |
| // between 'x' and 'a', because the click was after the second character of |
| // the unelided text "example.com". |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(14U, start); |
| EXPECT_EQ(14U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, MouseDoubleClick) { |
| SendMouseClick(4 * kCharacterWidth); |
| |
| // Second click without advancing the clock should be a double-click, which |
| // should do a single word selection and unelide the text on mousedown. |
| omnibox_view()->OnMousePressed(CreateMouseEvent( |
| ui::ET_MOUSE_PRESSED, GetPointInTextAtXOffset(4 * kCharacterWidth))); |
| ExpectFullUrlDisplayed(); |
| |
| // Verify that the selection is https://www.|example|.com, since the |
| // double-click after the fourth character of the unelided text "example.com". |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(12U, start); |
| EXPECT_EQ(19U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, MouseTripleClick) { |
| SendMouseClick(4 * kCharacterWidth); |
| SendMouseClick(4 * kCharacterWidth); |
| SendMouseClick(4 * kCharacterWidth); |
| |
| ExpectFullUrlDisplayed(); |
| |
| // Verify that the whole full URL is selected. |
| EXPECT_TRUE(omnibox_view()->IsSelectAll()); |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(0U, start); |
| EXPECT_EQ(24U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, MouseClickDrag) { |
| omnibox_view()->OnMousePressed(CreateMouseEvent( |
| ui::ET_MOUSE_PRESSED, GetPointInTextAtXOffset(2 * kCharacterWidth))); |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| |
| // Expect that during the drag, the URL is still elided. |
| omnibox_view()->OnMouseDragged(CreateMouseEvent( |
| ui::ET_MOUSE_DRAGGED, GetPointInTextAtXOffset(4 * kCharacterWidth))); |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| |
| // Expect that ex|am|ple.com is the drag selected portion while dragging. |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(2U, start); |
| EXPECT_EQ(4U, end); |
| |
| omnibox_view()->OnMouseReleased(CreateMouseEvent( |
| ui::ET_MOUSE_RELEASED, GetPointInTextAtXOffset(4 * kCharacterWidth))); |
| ExpectFullUrlDisplayed(); |
| |
| // Expect that https://www.ex|am|ple.com is the selected portion after the |
| // user releases the mouse. |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(14U, start); |
| EXPECT_EQ(16U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, |
| MouseClickDragToBeginningSelectingText) { |
| // Backwards drag-select this portion of the elided URL: |exam|ple.com |
| omnibox_view()->OnMousePressed(CreateMouseEvent( |
| ui::ET_MOUSE_PRESSED, GetPointInTextAtXOffset(4 * kCharacterWidth))); |
| omnibox_view()->OnMouseDragged(CreateMouseEvent( |
| ui::ET_MOUSE_DRAGGED, GetPointInTextAtXOffset(0 * kCharacterWidth))); |
| omnibox_view()->OnMouseReleased(CreateMouseEvent( |
| ui::ET_MOUSE_RELEASED, GetPointInTextAtXOffset(0 * kCharacterWidth))); |
| ExpectFullUrlDisplayed(); |
| |
| // Since the selection did not look like a URL, expect the following selected |
| // selected portion after the user releases the mouse: |
| // https://www.|exam|ple.com |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(16U, start); |
| EXPECT_EQ(12U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, |
| MouseClickDragToBeginningSelectingURL) { |
| // Backwards drag-select this portion of the elided URL: |example.co|m |
| omnibox_view()->OnMousePressed(CreateMouseEvent( |
| ui::ET_MOUSE_PRESSED, GetPointInTextAtXOffset(10 * kCharacterWidth))); |
| omnibox_view()->OnMouseDragged(CreateMouseEvent( |
| ui::ET_MOUSE_DRAGGED, GetPointInTextAtXOffset(0 * kCharacterWidth))); |
| omnibox_view()->OnMouseReleased(CreateMouseEvent( |
| ui::ET_MOUSE_RELEASED, GetPointInTextAtXOffset(0 * kCharacterWidth))); |
| ExpectFullUrlDisplayed(); |
| |
| // Since the selection does look like a URL, expect the following selected |
| // selected portion after the user releases the mouse: |
| // |https://www.example.co|m |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(22U, start); |
| EXPECT_EQ(0U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, MouseDoubleClickDrag) { |
| // Expect that after a double-click after the third character of the elided |
| // text, the text is unelided, and https://www.|example|.com is selected. |
| SendMouseClick(4 * kCharacterWidth); |
| omnibox_view()->OnMousePressed(CreateMouseEvent( |
| ui::ET_MOUSE_PRESSED, GetPointInTextAtXOffset(4 * kCharacterWidth))); |
| ExpectFullUrlDisplayed(); |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(12U, start); |
| EXPECT_EQ(19U, end); |
| |
| // Expect that dragging to the fourth character of the full URL (between the |
| // the 'p' and the 's' of https), will word-select the scheme, subdomain, and |
| // domain, so the new selection will be |https://www.example|.com. The |
| // expected selection is backwards, since we are dragging the mouse from the |
| // domain to the scheme. |
| omnibox_view()->OnMouseDragged(CreateMouseEvent( |
| ui::ET_MOUSE_DRAGGED, GetPointInTextAtXOffset(2 * kCharacterWidth))); |
| ExpectFullUrlDisplayed(); |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(19U, start); |
| EXPECT_EQ(0U, end); |
| |
| // Expect the selection to stay the same after mouse-release. |
| omnibox_view()->OnMouseReleased(CreateMouseEvent( |
| ui::ET_MOUSE_RELEASED, GetPointInTextAtXOffset(2 * kCharacterWidth))); |
| ExpectFullUrlDisplayed(); |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(19U, start); |
| EXPECT_EQ(0U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, ReelideOnBlur) { |
| // Double-click should unelide the URL by making a partial selection. |
| SendMouseClick(4 * kCharacterWidth); |
| SendMouseClick(4 * kCharacterWidth); |
| ExpectFullUrlDisplayed(); |
| |
| BlurOmnibox(); |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, DontReelideOnBlurIfEdited) { |
| // Double-click should unelide the URL by making a partial selection. |
| SendMouseClick(4 * kCharacterWidth); |
| SendMouseClick(4 * kCharacterWidth); |
| ExpectFullUrlDisplayed(); |
| |
| // Since the domain word is selected, pressing 'a' should replace the domain. |
| ui::KeyEvent char_event(ui::ET_KEY_PRESSED, ui::VKEY_A, ui::DomCode::US_A, 0, |
| ui::DomKey::FromCharacter('a'), |
| ui::EventTimeForNow()); |
| omnibox_textfield()->InsertChar(char_event); |
| EXPECT_EQ(base::ASCIIToUTF16("https://www.a.com/"), omnibox_view()->text()); |
| EXPECT_TRUE(omnibox_view()->model()->user_input_in_progress()); |
| |
| // Now that we've edited the text, blurring should not re-elide the URL. |
| BlurOmnibox(); |
| EXPECT_EQ(base::ASCIIToUTF16("https://www.a.com/"), omnibox_view()->text()); |
| EXPECT_TRUE(omnibox_view()->model()->user_input_in_progress()); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, |
| DontReelideOnBlurIfWidgetDeactivated) { |
| SendMouseClick(0); |
| SendMouseClick(0); |
| ExpectFullUrlDisplayed(); |
| |
| // Create a different Widget that will take focus away from the test widget |
| // containing our test Omnibox. |
| auto other_widget = std::make_unique<views::Widget>(); |
| views::Widget::InitParams params = |
| CreateParams(views::Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.bounds = gfx::Rect(0, 0, 100, 100); |
| other_widget->Init(params); |
| other_widget->Show(); |
| ExpectFullUrlDisplayed(); |
| |
| omnibox_view()->GetWidget()->Activate(); |
| ExpectFullUrlDisplayed(); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, SaveSelectAllOnBlurAndRefocus) { |
| SendMouseClick(0); |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| EXPECT_TRUE(omnibox_view()->IsSelectAll()); |
| |
| // Blurring and refocusing should preserve a select-all state. |
| BlurOmnibox(); |
| omnibox_view()->RequestFocus(); |
| EXPECT_TRUE(omnibox_view()->HasFocus()); |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| EXPECT_TRUE(omnibox_view()->IsSelectAll()); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsTest, UnelideFromModel) { |
| EXPECT_TRUE(IsElidedUrlDisplayed()); |
| |
| omnibox_view()->model()->Unelide(false /* exit_query_in_omnibox */); |
| EXPECT_TRUE(omnibox_view()->IsSelectAll()); |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(24U, start); |
| EXPECT_EQ(0U, end); |
| ExpectFullUrlDisplayed(); |
| } |
| |
| class OmniboxViewViewsSteadyStateElisionsAndQueryInOmniboxTest |
| : public OmniboxViewViewsSteadyStateElisionsTest { |
| public: |
| OmniboxViewViewsSteadyStateElisionsAndQueryInOmniboxTest() |
| : OmniboxViewViewsSteadyStateElisionsTest({ |
| omnibox::kHideSteadyStateUrlScheme, |
| omnibox::kHideSteadyStateUrlTrivialSubdomains, |
| omnibox::kQueryInOmnibox, |
| }) {} |
| |
| protected: |
| const GURL kValidSearchResultsPage = |
| GURL("https://www.google.com/search?q=foo+query"); |
| |
| void SetUp() override { |
| OmniboxViewViewsSteadyStateElisionsTest::SetUp(); |
| |
| location_bar_model()->set_url(kValidSearchResultsPage); |
| location_bar_model()->set_security_level( |
| security_state::SecurityLevel::SECURE); |
| location_bar_model()->set_display_search_terms( |
| base::ASCIIToUTF16("foo query")); |
| |
| omnibox_view()->model()->ResetDisplayTexts(); |
| omnibox_view()->RevertAll(); |
| |
| // Sanity check that Query in Omnibox is working with Steady State Elisions. |
| EXPECT_EQ(base::ASCIIToUTF16("foo query"), omnibox_view()->text()); |
| |
| // Focus the Omnibox. |
| SendMouseClick(0); |
| } |
| }; |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsAndQueryInOmniboxTest, |
| DontUnelideQueryInOmniboxSearchTerms) { |
| // Right key should NOT unelide, and should correctly place the cursor at the |
| // end of the search query. |
| omnibox_textfield_view()->OnKeyPressed( |
| ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_RIGHT, 0)); |
| EXPECT_EQ(base::ASCIIToUTF16("foo query"), omnibox_view()->text()); |
| EXPECT_FALSE(omnibox_view()->model()->user_input_in_progress()); |
| |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(9U, start); |
| EXPECT_EQ(9U, end); |
| } |
| |
| TEST_F(OmniboxViewViewsSteadyStateElisionsAndQueryInOmniboxTest, |
| UnelideFromModel) { |
| // Uneliding without exiting Query in Omnibox should do nothing. |
| omnibox_view()->model()->Unelide(false /* exit_query_in_omnibox */); |
| EXPECT_EQ(base::ASCIIToUTF16("foo query"), omnibox_view()->text()); |
| { |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(9U, start); |
| EXPECT_EQ(0U, end); |
| } |
| |
| // Uneliding and exiting Query in Omnibox should reveal the full URL. |
| omnibox_view()->model()->Unelide(true /* exit_query_in_omnibox */); |
| EXPECT_EQ(base::ASCIIToUTF16(kValidSearchResultsPage.spec()), |
| omnibox_view()->text()); |
| { |
| size_t start, end; |
| omnibox_view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(41U, start); |
| EXPECT_EQ(0U, end); |
| } |
| } |