| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/omnibox/browser/omnibox_edit_model.h" |
| |
| #include <stddef.h> |
| |
| #include <array> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "build/build_config.h" |
| #include "components/dom_distiller/core/url_constants.h" |
| #include "components/dom_distiller/core/url_utils.h" |
| #include "components/omnibox/browser/actions/omnibox_action.h" |
| #include "components/omnibox/browser/actions/tab_switch_action.h" |
| #include "components/omnibox/browser/autocomplete_controller.h" |
| #include "components/omnibox/browser/autocomplete_match.h" |
| #include "components/omnibox/browser/fake_autocomplete_controller.h" |
| #include "components/omnibox/browser/omnibox_controller.h" |
| #include "components/omnibox/browser/omnibox_popup_view.h" |
| #include "components/omnibox/browser/omnibox_prefs.h" |
| #include "components/omnibox/browser/omnibox_view.h" |
| #include "components/omnibox/browser/search_provider.h" |
| #include "components/omnibox/browser/test_location_bar_model.h" |
| #include "components/omnibox/browser/test_omnibox_client.h" |
| #include "components/omnibox/browser/test_omnibox_edit_model.h" |
| #include "components/omnibox/browser/test_omnibox_popup_view.h" |
| #include "components/omnibox/browser/test_omnibox_view.h" |
| #include "components/omnibox/browser/test_scheme_classifier.h" |
| #include "components/omnibox/browser/unscoped_extension_provider.h" |
| #include "components/omnibox/common/omnibox_feature_configs.h" |
| #include "components/omnibox/common/omnibox_features.h" |
| #include "components/omnibox/common/omnibox_focus_state.h" |
| #include "components/sync_preferences/testing_pref_service_syncable.h" |
| #include "components/url_formatter/url_fixer.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "omnibox_triggered_feature_service.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/metrics_proto/omnibox_event.pb.h" |
| #include "third_party/omnibox_proto/answer_type.pb.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/window_open_disposition.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/image/image_unittest_util.h" |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "extensions/common/extension_features.h" // nogncheck |
| #endif |
| |
| using metrics::OmniboxEventProto; |
| using Selection = OmniboxPopupSelection; |
| using testing::_; |
| using testing::DoAll; |
| using testing::Return; |
| using testing::SaveArg; |
| |
| namespace ui { |
| struct AXNodeData; |
| } |
| |
| namespace { |
| |
| void OpenUrlFromEditBox(OmniboxController* controller, |
| TestOmniboxEditModel* model, |
| const std::u16string url_text, |
| bool is_autocompleted) { |
| AutocompleteMatch match( |
| controller->autocomplete_controller()->search_provider(), 0, false, |
| AutocompleteMatchType::OPEN_TAB); |
| match.destination_url = GURL(url_text); |
| match.allowed_to_be_default_match = true; |
| if (is_autocompleted) { |
| match.inline_autocompletion = url_text; |
| } else { |
| model->SetUserText(url_text); |
| } |
| model->OnSetFocus(false); |
| model->OpenMatchForTesting(match, WindowOpenDisposition::CURRENT_TAB, GURL(), |
| std::u16string(), 0); |
| } |
| |
| } // namespace |
| |
| class OmniboxEditModelTest : public testing::Test { |
| public: |
| OmniboxEditModelTest() { |
| view_ = std::make_unique<TestOmniboxView>( |
| std::make_unique<TestOmniboxClient>()); |
| view_->controller()->SetEditModelForTesting( |
| std::make_unique<TestOmniboxEditModel>(view_->controller(), view_.get(), |
| pref_service())); |
| |
| EXPECT_CALL(*client(), GetPrefs()).WillRepeatedly(Return(pref_service())); |
| } |
| |
| void SetUp() override { |
| omnibox::RegisterProfilePrefs( |
| static_cast<sync_preferences::TestingPrefServiceSyncable*>( |
| pref_service()) |
| ->registry()); |
| omnibox::RegisterProfilePrefs( |
| static_cast<sync_preferences::TestingPrefServiceSyncable*>( |
| classifier_pref_service()) |
| ->registry()); |
| } |
| |
| PrefService* pref_service() { |
| return controller() |
| ->autocomplete_controller() |
| ->autocomplete_provider_client() |
| ->GetPrefs(); |
| } |
| PrefService* classifier_pref_service() { |
| return client() |
| ->autocomplete_classifier() |
| ->autocomplete_controller() |
| ->autocomplete_provider_client() |
| ->GetPrefs(); |
| } |
| TestOmniboxView* view() { return view_.get(); } |
| TestLocationBarModel* location_bar_model() { |
| return client()->location_bar_model(); |
| } |
| TestOmniboxEditModel* model() { |
| return static_cast<TestOmniboxEditModel*>(view_->model()); |
| } |
| OmniboxController* controller() { return view_->controller(); } |
| TestOmniboxClient* client() { |
| return static_cast<TestOmniboxClient*>(controller()->client()); |
| } |
| |
| protected: |
| base::test::TaskEnvironment task_environment_; |
| std::unique_ptr<TestOmniboxView> view_; |
| }; |
| |
| TEST_F(OmniboxEditModelTest, DISABLED_InlineAutocompleteText) { |
| // Test if the model updates the inline autocomplete text in the view. |
| EXPECT_EQ(std::u16string(), view()->inline_autocompletion()); |
| model()->SetUserText(u"he"); |
| model()->OnPopupDataChanged(std::u16string(), |
| /*is_temporary_text=*/false, u"llo", |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| EXPECT_EQ(u"hello", view()->GetText()); |
| EXPECT_EQ(u"llo", view()->inline_autocompletion()); |
| |
| std::u16string text_before = u"he"; |
| std::u16string text_after = u"hel"; |
| OmniboxView::StateChanges state_changes{ |
| &text_before, &text_after, 3, 3, false, true, false, false}; |
| model()->OnAfterPossibleChange(state_changes, true); |
| EXPECT_EQ(std::u16string(), view()->inline_autocompletion()); |
| model()->OnPopupDataChanged(std::u16string(), |
| /*is_temporary_text=*/false, u"lo", |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| EXPECT_EQ(u"hello", view()->GetText()); |
| EXPECT_EQ(u"lo", view()->inline_autocompletion()); |
| |
| model()->Revert(); |
| EXPECT_EQ(std::u16string(), view()->GetText()); |
| EXPECT_EQ(std::u16string(), view()->inline_autocompletion()); |
| |
| model()->SetUserText(u"he"); |
| model()->OnPopupDataChanged(std::u16string(), |
| /*is_temporary_text=*/false, u"llo", |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| EXPECT_EQ(u"hello", view()->GetText()); |
| EXPECT_EQ(u"llo", view()->inline_autocompletion()); |
| |
| model()->AcceptTemporaryTextAsUserText(); |
| EXPECT_EQ(u"hello", view()->GetText()); |
| EXPECT_EQ(std::u16string(), view()->inline_autocompletion()); |
| } |
| |
| // iOS doesn't use elisions in the Omnibox textfield. |
| #if !BUILDFLAG(IS_IOS) |
| TEST_F(OmniboxEditModelTest, RespectUnelisionInZeroSuggest) { |
| location_bar_model()->set_url(GURL("https://www.example.com/")); |
| location_bar_model()->set_url_for_display(u"example.com"); |
| |
| EXPECT_TRUE(model()->ResetDisplayTexts()); |
| model()->Revert(); |
| |
| // Set up view with unelided text. |
| EXPECT_EQ(u"example.com", view()->GetText()); |
| EXPECT_TRUE(model()->Unelide()); |
| EXPECT_EQ(u"https://www.example.com/", view()->GetText()); |
| EXPECT_FALSE(model()->user_input_in_progress()); |
| EXPECT_TRUE(view()->IsSelectAll()); |
| |
| // Test that we don't clobber the unelided text with inline autocomplete text. |
| EXPECT_EQ(std::u16string(), view()->inline_autocompletion()); |
| model()->StartZeroSuggestRequest(); |
| model()->OnPopupDataChanged(std::u16string(), /*is_temporary_text=*/false, |
| std::u16string(), std::u16string(), |
| std::u16string(), false, std::u16string(), {}); |
| EXPECT_EQ(u"https://www.example.com/", view()->GetText()); |
| EXPECT_FALSE(model()->user_input_in_progress()); |
| EXPECT_TRUE(view()->IsSelectAll()); |
| } |
| #endif // !BUILDFLAG(IS_IOS) |
| |
| TEST_F(OmniboxEditModelTest, RevertZeroSuggestTemporaryText) { |
| location_bar_model()->set_url(GURL("https://www.example.com/")); |
| location_bar_model()->set_url_for_display(u"https://www.example.com/"); |
| |
| EXPECT_TRUE(model()->ResetDisplayTexts()); |
| model()->Revert(); |
| |
| // Simulate getting ZeroSuggestions and arrowing to a different match. |
| view()->SelectAll(true); |
| model()->StartZeroSuggestRequest(); |
| model()->OnPopupDataChanged(u"fake_temporary_text", |
| /*is_temporary_text=*/true, std::u16string(), |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| |
| // Test that reverting brings back the original input text. |
| EXPECT_TRUE(model()->OnEscapeKeyPressed()); |
| EXPECT_EQ(u"https://www.example.com/", view()->GetText()); |
| EXPECT_FALSE(model()->user_input_in_progress()); |
| EXPECT_TRUE(view()->IsSelectAll()); |
| } |
| |
| // This verifies the fix for a bug where calling OpenMatch() with a valid |
| // alternate nav URL would fail a DCHECK if the input began with "http://". |
| // The failure was due to erroneously trying to strip the scheme from the |
| // resulting fill_into_edit. Alternate nav matches are never shown, so there's |
| // no need to ever try and strip this scheme. |
| TEST_F(OmniboxEditModelTest, AlternateNavHasHTTP) { |
| AutocompleteMatch match( |
| controller()->autocomplete_controller()->search_provider(), 0, false, |
| AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED); |
| // |match.destination_url| has to be set to ensure that OnAutocompleteAccept |
| // is called and |alternate_nav_match| is populated. |
| match.destination_url = GURL("https://foo/"); |
| const GURL alternate_nav_url("http://abcd/"); |
| |
| AutocompleteMatch alternate_nav_match; |
| EXPECT_CALL(*client(), OnAutocompleteAccept(_, _, _, _, _, _, _, _, _, _, _)) |
| .WillOnce(SaveArg<10>(&alternate_nav_match)); |
| |
| model()->OnSetFocus(false); // Avoids DCHECK in OpenMatch(). |
| model()->SetUserText(u"http://abcd"); |
| model()->OpenMatchForTesting(match, WindowOpenDisposition::CURRENT_TAB, |
| alternate_nav_url, std::u16string(), 0); |
| EXPECT_TRUE( |
| AutocompleteInput::HasHTTPScheme(alternate_nav_match.fill_into_edit)); |
| |
| EXPECT_CALL(*client(), OnAutocompleteAccept(_, _, _, _, _, _, _, _, _, _, _)) |
| .WillOnce(SaveArg<10>(&alternate_nav_match)); |
| |
| model()->SetUserText(u"abcd"); |
| model()->OpenMatchForTesting(match, WindowOpenDisposition::CURRENT_TAB, |
| alternate_nav_url, std::u16string(), 0); |
| EXPECT_TRUE( |
| AutocompleteInput::HasHTTPScheme(alternate_nav_match.fill_into_edit)); |
| } |
| |
| TEST_F(OmniboxEditModelTest, CurrentMatch) { |
| // Test the HTTP case. |
| { |
| location_bar_model()->set_url(GURL("http://www.example.com/")); |
| location_bar_model()->set_url_for_display(u"example.com"); |
| model()->ResetDisplayTexts(); |
| model()->Revert(); |
| |
| // iOS doesn't do elision in the textfield view. |
| #if BUILDFLAG(IS_IOS) |
| EXPECT_EQ(u"http://www.example.com/", view()->GetText()); |
| #else |
| EXPECT_EQ(u"example.com", view()->GetText()); |
| #endif |
| |
| AutocompleteMatch match = model()->CurrentMatch(nullptr); |
| EXPECT_EQ(AutocompleteMatchType::URL_WHAT_YOU_TYPED, match.type); |
| EXPECT_TRUE(model()->CurrentTextIsURL()); |
| EXPECT_EQ("http://www.example.com/", match.destination_url.spec()); |
| } |
| |
| // Test that generating a match from an elided HTTPS URL doesn't drop the |
| // secure scheme. |
| { |
| location_bar_model()->set_url(GURL("https://www.google.com/")); |
| location_bar_model()->set_url_for_display(u"google.com"); |
| model()->ResetDisplayTexts(); |
| model()->Revert(); |
| |
| // iOS doesn't do elision in the textfield view. |
| #if BUILDFLAG(IS_IOS) |
| EXPECT_EQ(u"https://www.google.com/", view()->GetText()); |
| #else |
| EXPECT_EQ(u"google.com", view()->GetText()); |
| #endif |
| |
| AutocompleteMatch match = model()->CurrentMatch(nullptr); |
| EXPECT_EQ(AutocompleteMatchType::URL_WHAT_YOU_TYPED, match.type); |
| EXPECT_TRUE(model()->CurrentTextIsURL()); |
| |
| // Additionally verify we aren't accidentally dropping the HTTPS scheme. |
| EXPECT_EQ("https://www.google.com/", match.destination_url.spec()); |
| } |
| } |
| |
| TEST_F(OmniboxEditModelTest, DisplayText) { |
| location_bar_model()->set_url(GURL("https://www.example.com/")); |
| location_bar_model()->set_url_for_display(u"example.com"); |
| |
| EXPECT_TRUE(model()->ResetDisplayTexts()); |
| model()->Revert(); |
| |
| EXPECT_TRUE(model()->CurrentTextIsURL()); |
| |
| #if BUILDFLAG(IS_IOS) |
| // iOS OmniboxEditModel always provides the full URL as the OmniboxView |
| // permanent display text. Unelision should return false. |
| EXPECT_EQ(u"https://www.example.com/", model()->GetPermanentDisplayText()); |
| EXPECT_EQ(u"https://www.example.com/", view()->GetText()); |
| EXPECT_FALSE(model()->Unelide()); |
| EXPECT_FALSE(model()->user_input_in_progress()); |
| EXPECT_FALSE(view()->IsSelectAll()); |
| #else |
| // Verify we can unelide and show the full URL properly. |
| EXPECT_EQ(u"example.com", model()->GetPermanentDisplayText()); |
| EXPECT_EQ(u"example.com", view()->GetText()); |
| EXPECT_TRUE(model()->Unelide()); |
| EXPECT_FALSE(model()->user_input_in_progress()); |
| EXPECT_TRUE(view()->IsSelectAll()); |
| #endif |
| |
| EXPECT_EQ(u"https://www.example.com/", view()->GetText()); |
| EXPECT_TRUE(model()->CurrentTextIsURL()); |
| |
| // We should still show the current page's icon until the URL is modified. |
| EXPECT_TRUE(model()->ShouldShowCurrentPageIcon()); |
| view()->SetUserText(u"something else"); |
| EXPECT_FALSE(model()->ShouldShowCurrentPageIcon()); |
| } |
| |
| TEST_F(OmniboxEditModelTest, UnelideDoesNothingWhenFullURLAlreadyShown) { |
| location_bar_model()->set_url(GURL("https://www.example.com/")); |
| location_bar_model()->set_url_for_display(u"https://www.example.com/"); |
| |
| EXPECT_TRUE(model()->ResetDisplayTexts()); |
| model()->Revert(); |
| |
| EXPECT_EQ(u"https://www.example.com/", model()->GetPermanentDisplayText()); |
| EXPECT_TRUE(model()->CurrentTextIsURL()); |
| |
| // Verify Unelide does nothing. |
| EXPECT_FALSE(model()->Unelide()); |
| EXPECT_EQ(u"https://www.example.com/", view()->GetText()); |
| EXPECT_FALSE(model()->user_input_in_progress()); |
| EXPECT_FALSE(view()->IsSelectAll()); |
| EXPECT_TRUE(model()->CurrentTextIsURL()); |
| EXPECT_TRUE(model()->ShouldShowCurrentPageIcon()); |
| } |
| |
| // The tab-switching system sometimes focuses the Omnibox even if it was not |
| // previously focused. In those cases, ignore the saved focus state. |
| TEST_F(OmniboxEditModelTest, IgnoreInvalidSavedFocusStates) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures( |
| {}, {omnibox::kOmniboxRestoreInvisibleFocusOnly}); |
| |
| // The Omnibox starts out unfocused. Save that state. |
| ASSERT_FALSE(model()->has_focus()); |
| OmniboxEditModel::State state = model()->GetStateForTabSwitch(); |
| ASSERT_EQ(OMNIBOX_FOCUS_NONE, state.focus_state); |
| |
| // Simulate the tab-switching system focusing the Omnibox. |
| model()->OnSetFocus(false); |
| |
| // Restoring the old saved state should not clobber the model's focus state. |
| model()->RestoreState(&state); |
| EXPECT_TRUE(model()->has_focus()); |
| EXPECT_TRUE(model()->is_caret_visible()); |
| } |
| |
| TEST_F(OmniboxEditModelTest, RestoreInvisibleFocusOnlyForVisibleState) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures( |
| {omnibox::kOmniboxRestoreInvisibleFocusOnly}, {}); |
| |
| // The Omnibox starts out focused. Save that state. |
| model()->OnSetFocus(false); |
| ASSERT_TRUE(model()->has_focus()); |
| OmniboxEditModel::State state = model()->GetStateForTabSwitch(); |
| ASSERT_EQ(OMNIBOX_FOCUS_VISIBLE, state.focus_state); |
| |
| // Remove focus from the Omnibox and confirm it no longer has focus. |
| model()->OnKillFocus(); |
| ASSERT_FALSE(model()->has_focus()); |
| |
| // Restoring the old saved state should not clobber the model's focus state. |
| model()->RestoreState(&state); |
| EXPECT_FALSE(model()->has_focus()); |
| } |
| |
| TEST_F(OmniboxEditModelTest, RestoreInvisibleFocusOnlyForInvisibleState) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures( |
| {omnibox::kOmniboxRestoreInvisibleFocusOnly}, {}); |
| |
| // The Omnibox starts out invisibly focused. Save that state. |
| model()->OnSetFocus(false); |
| model()->SetCaretVisibility(false); |
| ASSERT_TRUE(model()->has_focus()); |
| OmniboxEditModel::State state = model()->GetStateForTabSwitch(); |
| ASSERT_EQ(OMNIBOX_FOCUS_INVISIBLE, state.focus_state); |
| |
| // Remove focus from the Omnibox and confirm it no longer has focus. |
| model()->OnKillFocus(); |
| ASSERT_FALSE(model()->has_focus()); |
| |
| // Restoring the old saved state should clobber the model's focus state. |
| model()->RestoreState(&state); |
| EXPECT_TRUE(model()->has_focus()); |
| } |
| |
| // Tests ConsumeCtrlKey() consumes ctrl key when down, but does not affect ctrl |
| // state otherwise. |
| TEST_F(OmniboxEditModelTest, ConsumeCtrlKey) { |
| model()->control_key_state_ = TestOmniboxEditModel::UP; |
| model()->ConsumeCtrlKey(); |
| EXPECT_EQ(model()->control_key_state_, TestOmniboxEditModel::UP); |
| model()->control_key_state_ = TestOmniboxEditModel::DOWN; |
| model()->ConsumeCtrlKey(); |
| EXPECT_EQ(model()->control_key_state_, |
| TestOmniboxEditModel::DOWN_AND_CONSUMED); |
| model()->ConsumeCtrlKey(); |
| EXPECT_EQ(model()->control_key_state_, |
| TestOmniboxEditModel::DOWN_AND_CONSUMED); |
| } |
| |
| // Tests ctrl_key_state_ is set consumed if the ctrl key is down on focus. |
| TEST_F(OmniboxEditModelTest, ConsumeCtrlKeyOnRequestFocus) { |
| model()->control_key_state_ = TestOmniboxEditModel::DOWN; |
| model()->OnSetFocus(false); |
| EXPECT_EQ(model()->control_key_state_, TestOmniboxEditModel::UP); |
| model()->OnSetFocus(true); |
| EXPECT_EQ(model()->control_key_state_, |
| TestOmniboxEditModel::DOWN_AND_CONSUMED); |
| } |
| |
| // Tests the ctrl key is consumed on a ctrl-action (e.g. ctrl-c to copy) |
| TEST_F(OmniboxEditModelTest, ConsumeCtrlKeyOnCtrlAction) { |
| model()->control_key_state_ = TestOmniboxEditModel::DOWN; |
| OmniboxView::StateChanges state_changes{nullptr, nullptr, 0, 0, |
| false, false, false, false}; |
| model()->OnAfterPossibleChange(state_changes, false); |
| EXPECT_EQ(model()->control_key_state_, |
| TestOmniboxEditModel::DOWN_AND_CONSUMED); |
| } |
| |
| TEST_F(OmniboxEditModelTest, KeywordModePreservesInlineAutocompleteText) { |
| // Set the edit model into an inline autocompletion state. |
| view()->SetUserText(u"user"); |
| view()->OnInlineAutocompleteTextMaybeChanged(u"user", u" text"); |
| |
| // Entering keyword search mode should preserve the full display text as the |
| // user text, and select all. |
| model()->EnterKeywordModeForDefaultSearchProvider( |
| OmniboxEventProto::KEYBOARD_SHORTCUT); |
| EXPECT_EQ(u"user text", model()->GetUserTextForTesting()); |
| EXPECT_EQ(u"user text", view()->GetText()); |
| EXPECT_TRUE(view()->IsSelectAll()); |
| |
| // Deleting the user text (exiting keyword) mode should clear everything. |
| view()->SetUserText(std::u16string()); |
| { |
| EXPECT_TRUE(view()->GetText().empty()); |
| EXPECT_TRUE(model()->GetUserTextForTesting().empty()); |
| size_t start = 0, end = 0; |
| view()->GetSelectionBounds(&start, &end); |
| EXPECT_EQ(0U, start); |
| EXPECT_EQ(0U, end); |
| } |
| } |
| |
| TEST_F(OmniboxEditModelTest, KeywordModePreservesTemporaryText) { |
| // Set the edit model into a temporary text state. |
| view()->SetUserText(u"user text"); |
| GURL destination_url("http://example.com"); |
| |
| // OnPopupDataChanged() is called when the user focuses a suggestion. |
| model()->OnPopupDataChanged(u"match text", |
| /*is_temporary_text=*/true, std::u16string(), |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| |
| // Entering keyword search mode should preserve temporary text as the user |
| // text, and select all. |
| model()->EnterKeywordModeForDefaultSearchProvider( |
| OmniboxEventProto::KEYBOARD_SHORTCUT); |
| EXPECT_EQ(u"match text", model()->GetUserTextForTesting()); |
| EXPECT_EQ(u"match text", view()->GetText()); |
| EXPECT_TRUE(view()->IsSelectAll()); |
| } |
| |
| TEST_F(OmniboxEditModelTest, CtrlEnterNavigatesToDesiredTLD) { |
| // Set the edit model into an inline autocomplete state. |
| view()->SetUserText(u"foo"); |
| model()->StartAutocomplete(false, false); |
| view()->OnInlineAutocompleteTextMaybeChanged(u"foo", u"bar"); |
| |
| model()->OnControlKeyChanged(true); |
| model()->OpenSelection(); |
| OmniboxEditModel::State state = model()->GetStateForTabSwitch(); |
| EXPECT_EQ(GURL("http://www.foo.com/"), |
| state.autocomplete_input.canonicalized_url()); |
| } |
| |
| TEST_F(OmniboxEditModelTest, CtrlEnterNavigatesToDesiredTLDTemporaryText) { |
| // But if it's the temporary text, the View text should be used. |
| view()->SetUserText(u"foo"); |
| model()->StartAutocomplete(false, false); |
| model()->OnPopupDataChanged(u"foobar", |
| /*is_temporary_text=*/true, std::u16string(), |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| |
| model()->OnControlKeyChanged(true); |
| model()->OpenSelection(); |
| OmniboxEditModel::State state = model()->GetStateForTabSwitch(); |
| EXPECT_EQ(GURL("http://www.foobar.com/"), |
| state.autocomplete_input.canonicalized_url()); |
| } |
| |
| TEST_F(OmniboxEditModelTest, |
| CtrlEnterNavigatesToDesiredTLDSteadyStateElisions) { |
| location_bar_model()->set_url(GURL("https://www.example.com/")); |
| location_bar_model()->set_url_for_display(u"example.com"); |
| |
| EXPECT_TRUE(model()->ResetDisplayTexts()); |
| model()->Revert(); |
| |
| model()->OnControlKeyChanged(true); |
| model()->OpenSelection(); |
| OmniboxEditModel::State state = model()->GetStateForTabSwitch(); |
| EXPECT_EQ(GURL("https://www.example.com/"), |
| state.autocomplete_input.canonicalized_url()); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Popup-related tests |
| |
| class OmniboxEditModelPopupTest : public ::testing::Test { |
| public: |
| OmniboxEditModelPopupTest() { |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // `kExperimentalOmniboxLabs` feature flag has to be enabled |
| // before the test client initialization for the `UnscopedExtensionProvider` |
| // to be initialized. The provider is needed for |
| // `GetIconForExtensionWithImageURL` test. |
| feature_list_.InitAndEnableFeature( |
| extensions_features::kExperimentalOmniboxLabs); |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| view_ = std::make_unique<TestOmniboxView>( |
| std::make_unique<TestOmniboxClient>()); |
| view_->controller()->SetEditModelForTesting( |
| std::make_unique<TestOmniboxEditModel>(view_->controller(), view_.get(), |
| pref_service())); |
| |
| EXPECT_CALL(*client(), GetPrefs()).WillRepeatedly(Return(pref_service())); |
| |
| model()->set_popup_view(&popup_view_); |
| model()->SetPopupIsOpen(true); |
| } |
| |
| void SetUp() override { |
| omnibox::RegisterProfilePrefs( |
| static_cast<sync_preferences::TestingPrefServiceSyncable*>( |
| pref_service()) |
| ->registry()); |
| omnibox::RegisterProfilePrefs( |
| static_cast<sync_preferences::TestingPrefServiceSyncable*>( |
| classifier_pref_service()) |
| ->registry()); |
| } |
| |
| PrefService* pref_service() { |
| return controller() |
| ->autocomplete_controller() |
| ->autocomplete_provider_client() |
| ->GetPrefs(); |
| } |
| PrefService* classifier_pref_service() { |
| return client() |
| ->autocomplete_classifier() |
| ->autocomplete_controller() |
| ->autocomplete_provider_client() |
| ->GetPrefs(); |
| } |
| OmniboxTriggeredFeatureService* triggered_feature_service() { |
| return &triggered_feature_service_; |
| } |
| TestOmniboxEditModel* model() { |
| return static_cast<TestOmniboxEditModel*>(view_->model()); |
| } |
| OmniboxController* controller() { return view_->controller(); } |
| TestOmniboxClient* client() { |
| return static_cast<TestOmniboxClient*>(controller()->client()); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList feature_list_; |
| base::test::TaskEnvironment task_environment_; |
| std::unique_ptr<TestOmniboxView> view_; |
| TestOmniboxPopupView popup_view_; |
| OmniboxTriggeredFeatureService triggered_feature_service_; |
| }; |
| |
| // This verifies that the new treatment of the user's selected match in |
| // |SetSelectedLine()| with removed |AutocompleteResult::Selection::empty()| |
| // is correct in the face of various replacement versions of |empty()|. |
| TEST_F(OmniboxEditModelPopupTest, SetSelectedLine) { |
| ACMatches matches; |
| for (size_t i = 0; i < 2; ++i) { |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match.keyword = u"match"; |
| match.allowed_to_be_default_match = true; |
| matches.push_back(match); |
| } |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP, |
| TestSchemeClassifier()); |
| result->AppendMatches(matches); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| EXPECT_TRUE(model()->IsPopupSelectionOnInitialLine()); |
| model()->SetPopupSelection(Selection(0), true, false); |
| EXPECT_TRUE(model()->IsPopupSelectionOnInitialLine()); |
| model()->SetPopupSelection(Selection(0), false, false); |
| EXPECT_TRUE(model()->IsPopupSelectionOnInitialLine()); |
| } |
| |
| TEST_F(OmniboxEditModelPopupTest, |
| GetPopupAccessibilityLabelForCurrentSelection_KeywordMode) { |
| // Populate the TemplateURLService with starter pack entries. |
| std::vector<std::unique_ptr<TemplateURLData>> turls = |
| template_url_starter_pack_data::GetStarterPackEngines(); |
| for (auto& starter_turl : turls) { |
| controller()->client()->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(std::move(*starter_turl))); |
| } |
| |
| // Populate the TemplateURLService with site search entries. |
| TemplateURLData featured_data; |
| featured_data.SetShortName(u"SiteSearch"); |
| featured_data.SetKeyword(u"@sitesearch"); |
| featured_data.SetURL("https://sitesearch.com"); |
| TemplateURL* turl = controller()->client()->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(featured_data)); |
| ASSERT_TRUE(turl); |
| |
| TemplateURLData nonfeatured_data; |
| nonfeatured_data.SetShortName(u"SiteSearch"); |
| nonfeatured_data.SetKeyword(u"sitesearch"); |
| nonfeatured_data.SetURL("https://sitesearch.com"); |
| TemplateURL* nonfeatured_turl = |
| controller()->client()->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(nonfeatured_data)); |
| ASSERT_TRUE(nonfeatured_turl); |
| |
| // Create matches |
| AutocompleteMatch gemini_match(nullptr, 0, false, |
| AutocompleteMatchType::STARTER_PACK); |
| gemini_match.keyword = u"@gemini"; |
| gemini_match.associated_keyword = |
| std::make_unique<AutocompleteMatch>(gemini_match); |
| |
| AutocompleteMatch sitesearch_featured_match( |
| nullptr, 0, false, AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH); |
| sitesearch_featured_match.keyword = u"@sitesearch"; |
| sitesearch_featured_match.associated_keyword = |
| std::make_unique<AutocompleteMatch>(sitesearch_featured_match); |
| |
| AutocompleteMatch sitesearch_other_engine( |
| nullptr, 0, false, AutocompleteMatchType::SEARCH_OTHER_ENGINE); |
| sitesearch_other_engine.keyword = u"sitesearch"; |
| AutocompleteMatch sitesearch_nonfeatured_match( |
| nullptr, 0, false, AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED); |
| sitesearch_nonfeatured_match.keyword = u"google.com"; |
| sitesearch_nonfeatured_match.associated_keyword = |
| std::make_unique<AutocompleteMatch>(sitesearch_other_engine); |
| |
| // Create a result with matches. |
| ACMatches matches; |
| matches.push_back(gemini_match); |
| matches.push_back(sitesearch_featured_match); |
| matches.push_back(sitesearch_nonfeatured_match); |
| AutocompleteResult* result = |
| &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| // Test cases. |
| struct { |
| int line; |
| std::u16string input_text; |
| std::u16string expected_label; |
| } test_cases[] = { |
| {0, u"@gemini", u"@gemini, Ask Gemini"}, |
| {1, u"@sitesearch", u"@sitesearch, Search SiteSearch"}, |
| {2, u"sitesearch", u"Search SiteSearch"}, |
| }; |
| |
| int label_prefix_length = 0; |
| for (const auto& test_case : test_cases) { |
| model()->SetPopupSelection(OmniboxPopupSelection( |
| test_case.line, OmniboxPopupSelection::KEYWORD_MODE)); |
| std::u16string label = |
| model()->GetPopupAccessibilityLabelForCurrentSelection( |
| test_case.input_text, true, &label_prefix_length); |
| EXPECT_EQ(test_case.expected_label, label); |
| } |
| } |
| |
| TEST_F(OmniboxEditModelPopupTest, SetSelectedLineWithNoDefaultMatches) { |
| // Creates a set of matches with NO matches allowed to be default. |
| ACMatches matches; |
| for (size_t i = 0; i < 2; ++i) { |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match.keyword = u"match"; |
| matches.push_back(match); |
| } |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP, |
| TestSchemeClassifier()); |
| result->AppendMatches(matches); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| |
| model()->OnPopupResultChanged(); |
| EXPECT_EQ(Selection::kNoMatch, model()->GetPopupSelection().line); |
| EXPECT_TRUE(model()->IsPopupSelectionOnInitialLine()); |
| |
| model()->SetPopupSelection(Selection(0), false, false); |
| EXPECT_EQ(0U, model()->GetPopupSelection().line); |
| EXPECT_FALSE(model()->IsPopupSelectionOnInitialLine()); |
| |
| model()->SetPopupSelection(Selection(1), false, false); |
| EXPECT_EQ(1U, model()->GetPopupSelection().line); |
| EXPECT_FALSE(model()->IsPopupSelectionOnInitialLine()); |
| |
| model()->ResetPopupToInitialState(); |
| EXPECT_EQ(Selection::kNoMatch, model()->GetPopupSelection().line); |
| EXPECT_TRUE(model()->IsPopupSelectionOnInitialLine()); |
| } |
| |
| TEST_F(OmniboxEditModelPopupTest, PopupPositionChanging) { |
| ACMatches matches; |
| for (size_t i = 0; i < 3; ++i) { |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match.keyword = u"match"; |
| match.allowed_to_be_default_match = true; |
| matches.push_back(match); |
| } |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP, |
| TestSchemeClassifier()); |
| result->AppendMatches(matches); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| EXPECT_EQ(0u, model()->GetPopupSelection().line); |
| // Test moving and wrapping down. |
| for (size_t n : {1, 2, 0}) { |
| model()->OnUpOrDownPressed(true, false); |
| EXPECT_EQ(n, model()->GetPopupSelection().line); |
| } |
| // And down. |
| for (size_t n : {2, 1, 0}) { |
| model()->OnUpOrDownPressed(false, false); |
| EXPECT_EQ(n, model()->GetPopupSelection().line); |
| } |
| } |
| |
| #if !(BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)) |
| TEST_F(OmniboxEditModelPopupTest, PopupStepSelection) { |
| ACMatches matches; |
| for (size_t i = 0; i < 6; ++i) { |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match.keyword = u"match"; |
| match.allowed_to_be_default_match = true; |
| matches.push_back(match); |
| } |
| // Make the thumbs up/down selection available on match index 1. |
| matches[1].type = AutocompleteMatchType::HISTORY_EMBEDDINGS; |
| // Make match index 1 deletable to verify we can step to that. |
| matches[1].deletable = true; |
| // Make match index 2 only have an associated keyword to verify we can step |
| // backwards into keyword search mode if keyword search button is enabled. |
| matches[2].associated_keyword = |
| std::make_unique<AutocompleteMatch>(matches.back()); |
| // Make match index 3 have an associated keyword, tab match, and deletable to |
| // verify keyword mode doesn't override tab match and remove suggestion |
| // buttons (as it does with button row disabled) |
| matches[3].associated_keyword = |
| std::make_unique<AutocompleteMatch>(matches.back()); |
| matches[3].has_tab_match = true; |
| matches[3].deletable = true; |
| // Make match index 4 have a suggestion_group_id to test header behavior. |
| const auto kNewGroupId = omnibox::GROUP_PREVIOUS_SEARCH_RELATED; |
| matches[4].suggestion_group_id = kNewGroupId; |
| // Make match index 5 have a suggestion_group_id but no header text. |
| matches[5].suggestion_group_id = omnibox::GROUP_HISTORY_CLUSTER; |
| |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| omnibox::GroupConfigMap suggestion_groups_map; |
| suggestion_groups_map[kNewGroupId].set_header_text("header"); |
| suggestion_groups_map[omnibox::GROUP_HISTORY_CLUSTER].set_header_text(""); |
| |
| // Do not set the original_group_id on purpose to test that default visibility |
| // can be safely queried via OmniboxController::IsSuggestionGroupHidden(). |
| result->MergeSuggestionGroupsMap(suggestion_groups_map); |
| |
| AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP, |
| TestSchemeClassifier()); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| EXPECT_EQ(0u, model()->GetPopupSelection().line); |
| |
| // Step by lines forward. |
| for (size_t n : {1, 2, 3, 4, 5, 0}) { |
| model()->OnUpOrDownPressed(true, false); |
| EXPECT_EQ(n, model()->GetPopupSelection().line); |
| } |
| // Step by lines backward. |
| for (size_t n : {5, 4, 3, 2, 1, 0}) { |
| model()->OnUpOrDownPressed(false, false); |
| EXPECT_EQ(n, model()->GetPopupSelection().line); |
| } |
| |
| // Step by states forward. |
| for (auto selection : { |
| Selection(1, Selection::NORMAL), |
| Selection(1, Selection::FOCUSED_BUTTON_THUMBS_UP), |
| Selection(1, Selection::FOCUSED_BUTTON_THUMBS_DOWN), |
| Selection(1, Selection::FOCUSED_BUTTON_REMOVE_SUGGESTION), |
| Selection(2, Selection::NORMAL), |
| Selection(2, Selection::KEYWORD_MODE), |
| Selection(3, Selection::NORMAL), |
| Selection(3, Selection::KEYWORD_MODE), |
| Selection(3, Selection::FOCUSED_BUTTON_REMOVE_SUGGESTION), |
| Selection(4, Selection::NORMAL), |
| Selection(5, Selection::NORMAL), |
| Selection(0, Selection::NORMAL), |
| }) { |
| model()->OnTabPressed(false); |
| EXPECT_EQ(selection, model()->GetPopupSelection()); |
| } |
| // Step by states backward. Unlike prior to suggestion button row, there is |
| // no difference in behavior for KEYWORD mode moving forward or backward. |
| for (auto selection : { |
| Selection(5, Selection::NORMAL), |
| Selection(4, Selection::NORMAL), |
| Selection(3, Selection::FOCUSED_BUTTON_REMOVE_SUGGESTION), |
| Selection(3, Selection::KEYWORD_MODE), |
| Selection(3, Selection::NORMAL), |
| Selection(2, Selection::KEYWORD_MODE), |
| Selection(2, Selection::NORMAL), |
| Selection(1, Selection::FOCUSED_BUTTON_REMOVE_SUGGESTION), |
| Selection(1, Selection::FOCUSED_BUTTON_THUMBS_DOWN), |
| Selection(1, Selection::FOCUSED_BUTTON_THUMBS_UP), |
| Selection(1, Selection::NORMAL), |
| Selection(0, Selection::NORMAL), |
| Selection(5, Selection::NORMAL), |
| Selection(4, Selection::NORMAL), |
| Selection(3, Selection::FOCUSED_BUTTON_REMOVE_SUGGESTION), |
| }) { |
| model()->OnTabPressed(true); |
| EXPECT_EQ(selection, model()->GetPopupSelection()); |
| } |
| |
| // Try the `kAllLines` step behavior. |
| model()->OnUpOrDownPressed(false, true); |
| EXPECT_EQ(Selection(0, Selection::NORMAL), model()->GetPopupSelection()); |
| model()->OnUpOrDownPressed(true, true); |
| EXPECT_EQ(Selection(5, Selection::NORMAL), model()->GetPopupSelection()); |
| } |
| #endif // !(BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)) |
| |
| // Actions are not part of the selection stepping in Android and iOS at all. |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| TEST_F(OmniboxEditModelPopupTest, PopupStepSelectionWithActions) { |
| omnibox_feature_configs::ScopedConfigForTesting< |
| omnibox_feature_configs::Toolbelt> |
| scoped_config; |
| scoped_config.Get().enabled = true; |
| |
| ACMatches matches; |
| for (size_t i = 0; i < 4; ++i) { |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match.keyword = u"match"; |
| match.allowed_to_be_default_match = true; |
| matches.push_back(match); |
| } |
| |
| // The toolbelt match has three normal actions. |
| AutocompleteMatch toolbelt_match(nullptr, 1000, false, |
| AutocompleteMatchType::NULL_RESULT_MESSAGE); |
| toolbelt_match.actions.push_back(base::MakeRefCounted<OmniboxAction>( |
| OmniboxAction::LabelStrings(u"", u"", u"", u"foo"), GURL())); |
| toolbelt_match.actions.push_back(base::MakeRefCounted<OmniboxAction>( |
| OmniboxAction::LabelStrings(u"", u"", u"", u"bar"), GURL())); |
| toolbelt_match.actions.push_back(base::MakeRefCounted<OmniboxAction>( |
| OmniboxAction::LabelStrings(u"", u"", u"", u"spam"), GURL())); |
| matches.push_back(toolbelt_match); |
| |
| // The second match has a normal action. |
| matches[1].actions.push_back(base::MakeRefCounted<OmniboxAction>( |
| OmniboxAction::LabelStrings(), GURL())); |
| // The fourth match has an action that takes over the match. |
| matches[3].takeover_action = base::MakeRefCounted<OmniboxAction>( |
| OmniboxAction::LabelStrings(), GURL()); |
| |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP, |
| TestSchemeClassifier()); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| EXPECT_EQ(0u, model()->GetPopupSelection().line); |
| |
| // Step by lines forward. |
| for (size_t n : {1, 2, 3, 0}) { |
| model()->OnUpOrDownPressed(true, false); |
| EXPECT_EQ(n, model()->GetPopupSelection().line); |
| } |
| // Step by lines backward. |
| for (size_t n : {3, 2, 1, 0}) { |
| model()->OnUpOrDownPressed(false, false); |
| EXPECT_EQ(n, model()->GetPopupSelection().line); |
| } |
| |
| std::vector<std::u16string> expected_labels = {u"foo", u"bar", u"spam"}; |
| auto test_a11y_label = [&](size_t action_index, std::u16string a11y_label) { |
| DCHECK(action_index < expected_labels.size()); |
| EXPECT_EQ(expected_labels[action_index], a11y_label); |
| }; |
| |
| // Step by states forward. |
| for (auto selection : { |
| Selection(1, Selection::NORMAL), |
| Selection(1, Selection::FOCUSED_BUTTON_ACTION), |
| Selection(2, Selection::NORMAL), |
| Selection(3, Selection::NORMAL), |
| Selection(4, Selection::FOCUSED_BUTTON_ACTION, /*action_index=*/0), |
| Selection(4, Selection::FOCUSED_BUTTON_ACTION, /*action_index=*/1), |
| Selection(4, Selection::FOCUSED_BUTTON_ACTION, /*action_index=*/2), |
| Selection(0, Selection::NORMAL), |
| }) { |
| model()->OnTabPressed(false); |
| auto popup_selection = model()->GetPopupSelection(); |
| EXPECT_EQ(selection, popup_selection); |
| if (matches[popup_selection.line].IsToolbelt()) { |
| test_a11y_label(popup_selection.action_index, |
| model()->GetPopupAccessibilityLabelForCurrentSelection( |
| u"", false, nullptr)); |
| } |
| } |
| // Step by states backward. |
| for (auto selection : { |
| Selection(4, Selection::FOCUSED_BUTTON_ACTION, /*action_index=*/2), |
| Selection(4, Selection::FOCUSED_BUTTON_ACTION, /*action_index=*/1), |
| Selection(4, Selection::FOCUSED_BUTTON_ACTION, /*action_index=*/0), |
| Selection(3, Selection::NORMAL), |
| Selection(2, Selection::NORMAL), |
| Selection(1, Selection::FOCUSED_BUTTON_ACTION), |
| Selection(1, Selection::NORMAL), |
| Selection(0, Selection::NORMAL), |
| }) { |
| model()->OnTabPressed(true); |
| auto popup_selection = model()->GetPopupSelection(); |
| EXPECT_EQ(selection, popup_selection); |
| if (matches[popup_selection.line].IsToolbelt()) { |
| test_a11y_label(popup_selection.action_index, |
| model()->GetPopupAccessibilityLabelForCurrentSelection( |
| u"", false, nullptr)); |
| } |
| } |
| |
| // Try the `kAllLines` step behavior. |
| model()->OnUpOrDownPressed(false, true); |
| EXPECT_EQ(Selection(0, Selection::NORMAL), model()->GetPopupSelection()); |
| model()->OnUpOrDownPressed(true, true); |
| EXPECT_EQ(Selection(3, Selection::NORMAL), model()->GetPopupSelection()); |
| } |
| #endif |
| |
| TEST_F(OmniboxEditModelPopupTest, PopupInlineAutocompleteAndTemporaryText) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndDisableFeature(omnibox::kGroupingFrameworkForNonZPS); |
| // Create a set of three matches "a|1" (inline autocompleted), "a2", "a3". |
| // The third match has a suggestion group ID. |
| ACMatches matches; |
| for (size_t i = 0; i < 3; ++i) { |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::SEARCH_SUGGEST); |
| match.allowed_to_be_default_match = true; |
| matches.push_back(match); |
| } |
| |
| matches[0].fill_into_edit = u"a1"; |
| matches[0].inline_autocompletion = u"1"; |
| matches[1].fill_into_edit = u"a2"; |
| matches[2].fill_into_edit = u"a3"; |
| const auto kNewGroupId = omnibox::GROUP_PREVIOUS_SEARCH_RELATED; |
| matches[2].suggestion_group_id = kNewGroupId; |
| |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| omnibox::GroupConfigMap suggestion_groups_map; |
| suggestion_groups_map[kNewGroupId].set_header_text("header"); |
| // Do not set the original_group_id on purpose to test that default visibility |
| // can be safely queried via AutocompleteResult::IsSuggestionGroupHidden(). |
| result->MergeSuggestionGroupsMap(suggestion_groups_map); |
| |
| AutocompleteInput input(u"a", metrics::OmniboxEventProto::NTP, |
| TestSchemeClassifier()); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| |
| // Simulate OmniboxController updating the popup, then check initial state. |
| model()->OnPopupDataChanged(std::u16string(), |
| /*is_temporary_text=*/false, u"1", |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| EXPECT_EQ(Selection(0, Selection::NORMAL), model()->GetPopupSelection()); |
| EXPECT_EQ(u"1", model()->text()); |
| EXPECT_FALSE(model()->is_temporary_text()); |
| |
| // Tab down to second match. |
| model()->OnTabPressed(false); |
| EXPECT_EQ(Selection(1, Selection::NORMAL), model()->GetPopupSelection()); |
| EXPECT_EQ(u"a2", model()->text()); |
| EXPECT_TRUE(model()->is_temporary_text()); |
| |
| // Now tab down to the third match, and expect that we update the temporary |
| // text to the third match. |
| model()->OnTabPressed(false); |
| EXPECT_EQ(Selection(2, Selection::NORMAL), model()->GetPopupSelection()); |
| EXPECT_EQ(u"a3", model()->text()); |
| EXPECT_TRUE(model()->is_temporary_text()); |
| |
| // Now tab backwards to the second match, expect we update the temporary text |
| // to the second match. |
| model()->OnTabPressed(true); |
| EXPECT_EQ(Selection(1, Selection::NORMAL), model()->GetPopupSelection()); |
| EXPECT_EQ(u"a2", model()->text()); |
| EXPECT_TRUE(model()->is_temporary_text()); |
| } |
| |
| // Makes sure focus remains on the tab switch button when nothing changes, |
| // and leaves when it does. Exercises the ratcheting logic in |
| // OmniboxEditModel::OnPopupResultChanged(). |
| TEST_F(OmniboxEditModelPopupTest, TestFocusFixing) { |
| ACMatches matches; |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match.contents = u"match1.com"; |
| match.destination_url = GURL("http://match1.com"); |
| match.allowed_to_be_default_match = true; |
| match.has_tab_match = true; |
| matches.push_back(match); |
| |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP, |
| TestSchemeClassifier()); |
| result->AppendMatches(matches); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| model()->SetPopupSelection(Selection(0), true, false); |
| // The default state should be unfocused. |
| EXPECT_EQ(Selection::NORMAL, model()->GetPopupSelection().state); |
| |
| // Focus the selection. |
| model()->SetPopupSelection(Selection(0, Selection::FOCUSED_BUTTON_ACTION)); |
| EXPECT_EQ(Selection::FOCUSED_BUTTON_ACTION, |
| model()->GetPopupSelection().state); |
| |
| // Adding a match at end won't change that we selected first suggestion, so |
| // shouldn't change focused state. |
| matches[0].relevance = 999; |
| // Give it a different name so not deduped. |
| matches[0].contents = u"match2.com"; |
| matches[0].destination_url = GURL("http://match2.com"); |
| result->AppendMatches(matches); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| EXPECT_EQ(Selection::FOCUSED_BUTTON_ACTION, |
| model()->GetPopupSelection().state); |
| |
| // Changing selection should change focused state. |
| model()->SetPopupSelection(Selection(1)); |
| EXPECT_EQ(Selection::NORMAL, model()->GetPopupSelection().state); |
| |
| // Adding a match at end will reset selection to first, so should change |
| // selected line, and thus focus. |
| model()->SetPopupSelection(Selection(model()->GetPopupSelection().line, |
| Selection::FOCUSED_BUTTON_ACTION)); |
| matches[0].relevance = 999; |
| matches[0].contents = u"match3.com"; |
| matches[0].destination_url = GURL("http://match3.com"); |
| result->AppendMatches(matches); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| EXPECT_EQ(0U, model()->GetPopupSelection().line); |
| EXPECT_EQ(Selection::NORMAL, model()->GetPopupSelection().state); |
| |
| // Prepending a match won't change selection, but since URL is different, |
| // should clear the focus state. |
| model()->SetPopupSelection(Selection(model()->GetPopupSelection().line, |
| Selection::FOCUSED_BUTTON_ACTION)); |
| matches[0].relevance = 1100; |
| matches[0].contents = u"match4.com"; |
| matches[0].destination_url = GURL("http://match4.com"); |
| result->AppendMatches(matches); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| EXPECT_EQ(0U, model()->GetPopupSelection().line); |
| EXPECT_EQ(Selection::NORMAL, model()->GetPopupSelection().state); |
| |
| // Selecting |kNoMatch| should clear focus. |
| model()->SetPopupSelection(Selection(model()->GetPopupSelection().line, |
| Selection::FOCUSED_BUTTON_ACTION)); |
| model()->SetPopupSelection(Selection(Selection::kNoMatch)); |
| model()->OnPopupResultChanged(); |
| EXPECT_EQ(Selection::NORMAL, model()->GetPopupSelection().state); |
| } |
| |
| // Android and iOS handle actions and metrics differently from other platforms. |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| TEST_F(OmniboxEditModelPopupTest, OpenActionSelectionLogsOmniboxEvent) { |
| base::HistogramTester histogram_tester; |
| ACMatches matches; |
| for (size_t i = 0; i < 4; ++i) { |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match.keyword = u"match"; |
| match.allowed_to_be_default_match = true; |
| matches.push_back(match); |
| } |
| const GURL url = GURL("http://kong-foo.com"); |
| matches[1].destination_url = url; |
| matches[1].provider = |
| controller()->autocomplete_controller()->search_provider(); |
| matches[1].actions.push_back(base::MakeRefCounted<TabSwitchAction>(url)); |
| AutocompleteResult* result = |
| &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| AutocompleteInput input(u"match", metrics::OmniboxEventProto::NTP, |
| TestSchemeClassifier()); |
| result->SortAndCull(input, /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| model()->OnPopupResultChanged(); |
| model()->OpenSelection( |
| OmniboxPopupSelection(1, OmniboxPopupSelection::FOCUSED_BUTTON_ACTION)); |
| EXPECT_EQ(client()->last_log_disposition(), |
| WindowOpenDisposition::SWITCH_TO_TAB); |
| histogram_tester.ExpectUniqueSample("Omnibox.EventCount", 1, 1); |
| } |
| #endif |
| |
| TEST_F(OmniboxEditModelPopupTest, OpenThumbsDownSelectionShowsFeedback) { |
| // Set the input on the controller. |
| controller()->autocomplete_controller()->input_ = AutocompleteInput( |
| u"a", metrics::OmniboxEventProto::NTP, TestSchemeClassifier()); |
| |
| // Set the matches on the controller. |
| ACMatches matches; |
| { |
| AutocompleteMatch match(nullptr, 1000, false, |
| AutocompleteMatchType::SEARCH_SUGGEST); |
| match.allowed_to_be_default_match = true; |
| match.fill_into_edit = u"a1"; |
| match.inline_autocompletion = u"1"; |
| matches.push_back(match); |
| } |
| { |
| AutocompleteMatch match(nullptr, 999, false, |
| AutocompleteMatchType::HISTORY_EMBEDDINGS); |
| match.fill_into_edit = u"a2"; |
| match.destination_url = GURL("https://foo/"); |
| matches.push_back(match); |
| } |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| result->SortAndCull(controller()->autocomplete_controller()->input_, |
| /*template_url_service=*/nullptr, |
| triggered_feature_service(), /*is_lens_active=*/false, |
| /*can_show_contextual_suggestions=*/false, |
| /*mia_enabled*/ false); |
| |
| // Inform the model of the controller result set changes. |
| model()->OnPopupResultChanged(); |
| |
| // Simulate OmniboxController updating the popup, then check initial state. |
| model()->OnPopupDataChanged(std::u16string(), |
| /*is_temporary_text=*/false, u"a1", |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| EXPECT_EQ(Selection(0, Selection::NORMAL), model()->GetPopupSelection()); |
| EXPECT_EQ(u"a1", model()->text()); |
| EXPECT_FALSE(model()->is_temporary_text()); |
| |
| // Tab down to second match. |
| model()->OnTabPressed(false); |
| EXPECT_EQ(Selection(1, Selection::NORMAL), model()->GetPopupSelection()); |
| EXPECT_EQ(u"a2", model()->text()); |
| EXPECT_TRUE(model()->is_temporary_text()); |
| |
| // Tab to focus the thumbs up button. |
| model()->OnTabPressed(false); |
| EXPECT_EQ(Selection(1, Selection::FOCUSED_BUTTON_THUMBS_UP), |
| model()->GetPopupSelection()); |
| EXPECT_EQ(u"a2", model()->text()); |
| EXPECT_TRUE(model()->is_temporary_text()); |
| |
| EXPECT_EQ(FeedbackType::kNone, result->match_at(1)->feedback_type); |
| |
| // Simulate pressing the thumbs up button. |
| model()->OpenSelection(OmniboxPopupSelection( |
| 1, OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_UP)); |
| EXPECT_EQ(FeedbackType::kThumbsUp, result->match_at(1)->feedback_type); |
| |
| // Tab to focus the thumbs down button. |
| model()->OnTabPressed(false); |
| EXPECT_EQ(Selection(1, Selection::FOCUSED_BUTTON_THUMBS_DOWN), |
| model()->GetPopupSelection()); |
| EXPECT_EQ(u"a2", model()->text()); |
| EXPECT_TRUE(model()->is_temporary_text()); |
| |
| // Verify feedback form is requested only once. |
| std::u16string input_text; |
| GURL destination_url; |
| EXPECT_CALL(*client(), ShowFeedbackPage(_, _)) |
| .Times(1) |
| .WillOnce(DoAll(SaveArg<0>(&input_text), SaveArg<1>(&destination_url))); |
| |
| // Simulate pressing the thumbs down button. |
| model()->OpenSelection(OmniboxPopupSelection( |
| 1, OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_DOWN)); |
| EXPECT_EQ(FeedbackType::kThumbsDown, result->match_at(1)->feedback_type); |
| EXPECT_EQ(u"a", input_text); |
| EXPECT_EQ("https://foo/", destination_url.spec()); |
| |
| // Simulate pressing the thumbs down button. |
| model()->OpenSelection(OmniboxPopupSelection( |
| 1, OmniboxPopupSelection::FOCUSED_BUTTON_THUMBS_DOWN)); |
| EXPECT_EQ(FeedbackType::kNone, result->match_at(1)->feedback_type); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| // Tests the `GetMatchIcon()` method, verifying that a page favicon is used for |
| // `URL_WHAT_YOU_TYPED` matches. |
| TEST_F(OmniboxEditModelPopupTest, |
| GetMatchIconForUrlWhatYouTypedUsesPageFavicon) { |
| const GURL kUrl("https://foo.com"); |
| |
| GURL page_url; |
| EXPECT_CALL(*client(), GetFaviconForPageUrl(_, _)) |
| .WillOnce(DoAll(SaveArg<0>(&page_url), Return(gfx::Image()))); |
| EXPECT_CALL(*client(), GetFaviconForKeywordSearchProvider(_, _)).Times(0); |
| |
| AutocompleteMatch match; |
| match.type = AutocompleteMatchType::URL_WHAT_YOU_TYPED; |
| match.destination_url = kUrl; |
| |
| gfx::Image image = model()->GetMatchIcon(match, 0); |
| EXPECT_EQ(page_url, kUrl); |
| } |
| |
| // Tests the `GetMatchIcon()` method, verifying that a keyword favicon is used |
| // for `FEATURED_ENTERPRISE_SEARCH` matches with `kSiteSearch` policy origin. |
| TEST_F(OmniboxEditModelPopupTest, |
| GetMatchIconForFeaturedEnterpriseSiteSearchUsesKeywordFavicon) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(16, 16); |
| bitmap.eraseColor(SK_ColorRED); |
| gfx::Image expected_image = |
| gfx::Image(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); |
| |
| EXPECT_CALL(*client(), GetFaviconForPageUrl(_, _)).Times(0); |
| EXPECT_CALL(*client(), GetFaviconForKeywordSearchProvider(_, _)) |
| .WillOnce(Return(expected_image)); |
| |
| TemplateURLData data; |
| data.SetKeyword(u"sitesearch"); |
| data.SetURL("https://sitesearch.com"); |
| data.featured_by_policy = true; |
| data.policy_origin = TemplateURLData::PolicyOrigin::kSiteSearch; |
| TemplateURL* turl = controller()->client()->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(data)); |
| ASSERT_TRUE(turl); |
| |
| AutocompleteMatch match; |
| match.type = AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH; |
| match.destination_url = GURL("https://sitesearch.com"); |
| match.keyword = u"sitesearch"; |
| match.associated_keyword = std::make_unique<AutocompleteMatch>(match); |
| |
| gfx::Image image = model()->GetMatchIcon(match, 0); |
| gfx::test::CheckColors(bitmap.getColor(0, 0), |
| image.ToSkBitmap()->getColor(0, 0)); |
| } |
| |
| // Tests the `GetMatchIcon()` method, verifying that no favicon is used for |
| // `FEATURED_ENTERPRISE_SEARCH` matches with `kSearchAggregator` policy origin. |
| TEST_F(OmniboxEditModelPopupTest, |
| GetMatchIconForFeaturedEnterpriseSearchAggregator) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(16, 16); |
| bitmap.eraseColor(SK_ColorRED); |
| |
| EXPECT_CALL(*client(), GetFaviconForPageUrl(_, _)).Times(0); |
| EXPECT_CALL(*client(), GetFaviconForKeywordSearchProvider(_, _)).Times(0); |
| |
| TemplateURLData data; |
| data.SetKeyword(u"searchaggregator"); |
| data.SetURL("https://searchaggregator.com"); |
| data.featured_by_policy = true; |
| data.policy_origin = TemplateURLData::PolicyOrigin::kSearchAggregator; |
| TemplateURL* turl = controller()->client()->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(data)); |
| ASSERT_TRUE(turl); |
| |
| // Creates a set of matches. |
| ACMatches matches; |
| AutocompleteMatch search_aggregator_match( |
| nullptr, 1350, false, AutocompleteMatchType::FEATURED_ENTERPRISE_SEARCH); |
| search_aggregator_match.keyword = u"searchaggregator"; |
| search_aggregator_match.associated_keyword = |
| std::make_unique<AutocompleteMatch>(search_aggregator_match); |
| search_aggregator_match.icon_url = GURL("https://aggregator.com/icon.png"); |
| matches.push_back(search_aggregator_match); |
| AutocompleteMatch url_match(nullptr, 1000, false, |
| AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| url_match.keyword = u"match"; |
| matches.push_back(url_match); |
| AutocompleteResult* result = |
| &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| // Sets the icon bitmap for search aggregator match. |
| model()->SetIconBitmap(GURL("https://aggregator.com/icon.png"), bitmap); |
| |
| gfx::Image image = model()->GetMatchIcon(search_aggregator_match, 0); |
| gfx::test::CheckColors(bitmap.getColor(0, 0), |
| image.ToSkBitmap()->getColor(0, 0)); |
| } |
| |
| // Tests the `GetMatchIcon()` method, verifying that the icon served by a URL, |
| // if one is supplied with a content suggestion, is returned. |
| TEST_F(OmniboxEditModelPopupTest, |
| GetMatchIconForFeaturedEnterpriseSearchAggregatorContentSuggestion) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(16, 16); |
| bitmap.eraseColor(SK_ColorBLUE); |
| |
| // Creates a set of matches. |
| ACMatches matches; |
| AutocompleteMatch content_match(nullptr, 1000, false, |
| AutocompleteMatchType::NAVSUGGEST); |
| content_match.icon_url = GURL("https://example.com/icon.png"); |
| matches.push_back(content_match); |
| AutocompleteResult* result = |
| &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| // Sets the icon bitmap for content match. |
| model()->SetIconBitmap(GURL("https://example.com/icon.png"), bitmap); |
| |
| gfx::Image image = model()->GetMatchIcon(content_match, 0); |
| gfx::test::CheckColors(bitmap.getColor(0, 0), |
| image.ToSkBitmap()->getColor(0, 0)); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Tests the `GetMatchIcon()` method, verifying that the extension's icon is |
| // returned when no url is specified for the match. |
| TEST_F(OmniboxEditModelPopupTest, GetIconForExtensionWithNoImageURL) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(16, 16); |
| bitmap.eraseColor(SK_ColorRED); |
| gfx::Image expected_image = |
| gfx::Image(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); |
| |
| TemplateURLData data; |
| data.SetShortName(u"extension_name"); |
| data.SetKeyword(u"api"); |
| data.SetURL("https://extension.com"); |
| TemplateURL* turl = controller()->client()->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(data, TemplateURL::OMNIBOX_API_EXTENSION, |
| "extension_id", base::Time::Now(), false)); |
| ASSERT_TRUE(turl); |
| |
| EXPECT_CALL(*client(), GetExtensionIcon(_)).WillOnce(Return(expected_image)); |
| |
| AutocompleteMatch match( |
| controller()->autocomplete_controller()->unscoped_extension_provider(), 0, |
| false, AutocompleteMatchType::SEARCH_OTHER_ENGINE); |
| match.keyword = u"api"; |
| |
| gfx::Image image = model()->GetMatchIcon(match, 0); |
| gfx::test::CheckColors(bitmap.getColor(0, 0), |
| image.ToSkBitmap()->getColor(0, 0)); |
| } |
| |
| // Tests the `GetMatchIcon()` method, verifying that the favicon url from the |
| // extension match is returned. This simulates the case when the suggestion |
| // from an extension has a `faviconUrl` set. |
| TEST_F(OmniboxEditModelPopupTest, GetIconForExtensionWithImageURL) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(16, 16); |
| bitmap.eraseColor(SK_ColorRED); |
| gfx::Image expected_image = |
| gfx::Image(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); |
| |
| TemplateURLData data; |
| data.SetShortName(u"extension_name"); |
| data.SetKeyword(u"api"); |
| data.SetURL("https://extension.com"); |
| TemplateURL* turl = controller()->client()->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(data, TemplateURL::OMNIBOX_API_EXTENSION, |
| "extension_id", base::Time::Now(), false)); |
| ASSERT_TRUE(turl); |
| |
| EXPECT_CALL(*client(), GetExtensionIcon(_)).Times(0); |
| |
| AutocompleteMatch match( |
| controller()->autocomplete_controller()->unscoped_extension_provider(), 0, |
| false, AutocompleteMatchType::SEARCH_OTHER_ENGINE); |
| match.keyword = u"api"; |
| match.image_url = GURL("https://www.google-icon.com"); |
| match.provider = |
| controller()->autocomplete_controller()->unscoped_extension_provider(); |
| |
| // Creates a set of matches. |
| ACMatches matches; |
| matches.push_back(match); |
| AutocompleteResult* result = |
| &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| // Sets the popup rich suggestion bitmap for the extension match. |
| model()->SetPopupRichSuggestionBitmap(0, bitmap); |
| |
| gfx::Image image = model()->GetMatchIcon(match, 0); |
| gfx::test::CheckColors(bitmap.getColor(0, 0), |
| image.ToSkBitmap()->getColor(0, 0)); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| TEST_F(OmniboxEditModelTest, OmniboxEscapeHistogram) { |
| // Escape should incrementally revert temporary text, close the popup, clear |
| // input, and blur the omnibox. |
| AutocompleteMatch match; |
| match.type = AutocompleteMatchType::NAVSUGGEST; |
| match.destination_url = GURL("https://google.com"); |
| model()->SetCurrentMatchForTest(match); |
| |
| view()->SetUserText(u"user text"); |
| model()->OnSetFocus(false); |
| model()->SetInputInProgress(true); |
| model()->SetPopupIsOpen(true); |
| model()->OnPopupDataChanged(/*temporary_text=*/u"fake_temporary_text", |
| /*is_temporary_text=*/true, std::u16string(), |
| std::u16string(), std::u16string(), false, |
| std::u16string(), {}); |
| |
| EXPECT_TRUE(model()->HasTemporaryText()); |
| EXPECT_TRUE(model()->PopupIsOpen()); |
| EXPECT_EQ(view()->GetText(), u"fake_temporary_text"); |
| EXPECT_TRUE(model()->user_input_in_progress()); |
| EXPECT_TRUE(model()->has_focus()); |
| |
| { |
| // Revert temporary text. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE(model()->OnEscapeKeyPressed()); |
| histogram_tester.ExpectUniqueSample("Omnibox.Escape", 1, 1); |
| EXPECT_FALSE(model()->HasTemporaryText()); |
| EXPECT_TRUE(model()->PopupIsOpen()); |
| EXPECT_EQ(view()->GetText(), u""); |
| EXPECT_TRUE(model()->user_input_in_progress()); |
| EXPECT_TRUE(model()->has_focus()); |
| } |
| |
| { |
| // Close the popup. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE(model()->OnEscapeKeyPressed()); |
| histogram_tester.ExpectUniqueSample("Omnibox.Escape", 2, 1); |
| model()->SetPopupIsOpen(false); // `TestOmniboxEditModel` stubs the popup. |
| EXPECT_FALSE(model()->HasTemporaryText()); |
| EXPECT_FALSE(model()->PopupIsOpen()); |
| EXPECT_EQ(view()->GetText(), u""); |
| EXPECT_TRUE(model()->user_input_in_progress()); |
| EXPECT_TRUE(model()->has_focus()); |
| } |
| |
| { |
| // Clear user input. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE(model()->OnEscapeKeyPressed()); |
| histogram_tester.ExpectUniqueSample("Omnibox.Escape", 3, 1); |
| EXPECT_FALSE(model()->HasTemporaryText()); |
| EXPECT_FALSE(model()->PopupIsOpen()); |
| EXPECT_EQ(view()->GetText(), u""); |
| EXPECT_FALSE(model()->user_input_in_progress()); |
| EXPECT_TRUE(model()->has_focus()); |
| } |
| |
| { |
| // Blur the omnibox. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE(model()->OnEscapeKeyPressed()); |
| histogram_tester.ExpectUniqueSample("Omnibox.Escape", 5, 1); |
| model()->OnKillFocus(); // `TestOmniboxEditModel` stubs the client which |
| // handles blurring the omnibox. |
| EXPECT_FALSE(model()->HasTemporaryText()); |
| EXPECT_FALSE(model()->PopupIsOpen()); |
| EXPECT_EQ(view()->GetText(), u""); |
| EXPECT_FALSE(model()->user_input_in_progress()); |
| EXPECT_FALSE(model()->has_focus()); |
| } |
| } |
| |
| TEST_F(OmniboxEditModelTest, IPv4AddressPartsCount) { |
| base::HistogramTester histogram_tester; |
| constexpr char kIPv4AddressPartsCountHistogramName[] = |
| "Omnibox.IPv4AddressPartsCount"; |
| // Hostnames shall not be recorded. |
| OpenUrlFromEditBox(controller(), model(), u"http://example.com", false); |
| histogram_tester.ExpectTotalCount(kIPv4AddressPartsCountHistogramName, 0); |
| |
| // Autocompleted navigations shall not be recorded. |
| OpenUrlFromEditBox(controller(), model(), u"http://127.0.0.1", true); |
| histogram_tester.ExpectTotalCount(kIPv4AddressPartsCountHistogramName, 0); |
| |
| // Test IPv4 parts are correctly counted. |
| OpenUrlFromEditBox(controller(), model(), u"http://127.0.0.1", false); |
| OpenUrlFromEditBox(controller(), model(), u"http://127.1/test.html", false); |
| OpenUrlFromEditBox(controller(), model(), u"http://127.0.1", false); |
| EXPECT_THAT( |
| histogram_tester.GetAllSamples(kIPv4AddressPartsCountHistogramName), |
| testing::ElementsAre(base::Bucket(2, 1), base::Bucket(3, 1), |
| base::Bucket(4, 1))); |
| } |
| |
| #if !(BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)) |
| // The keyword mode feature is only available on Desktop. Do not test on mobile. |
| TEST_F(OmniboxEditModelTest, OpenTabMatch) { |
| // When the match comes from the Open Tab Provider while in keyword mode, |
| // the disposition should be set to SWITCH_TO_TAB. |
| AutocompleteMatch match( |
| controller()->autocomplete_controller()->open_tab_provider(), 0, false, |
| AutocompleteMatchType::OPEN_TAB); |
| match.destination_url = GURL("https://foo/"); |
| match.from_keyword = true; |
| |
| WindowOpenDisposition disposition; |
| EXPECT_CALL(*client(), OnAutocompleteAccept(_, _, _, _, _, _, _, _, _, _, _)) |
| .WillOnce(SaveArg<2>(&disposition)); |
| |
| model()->OnSetFocus(false); // Avoids DCHECK in OpenMatch(). |
| model()->SetUserText(u"http://abcd"); |
| model()->OpenMatchForTesting(match, WindowOpenDisposition::CURRENT_TAB, |
| GURL(), std::u16string(), 0); |
| EXPECT_EQ(disposition, WindowOpenDisposition::SWITCH_TO_TAB); |
| |
| EXPECT_CALL(*client(), OnAutocompleteAccept(_, _, _, _, _, _, _, _, _, _, _)) |
| .WillOnce(SaveArg<2>(&disposition)); |
| |
| // Suggestions not from the Open Tab Provider or not from keyword mode should |
| // not change the disposition. |
| match.from_keyword = false; |
| model()->OpenMatchForTesting(match, WindowOpenDisposition::CURRENT_TAB, |
| GURL(), std::u16string(), 0); |
| EXPECT_EQ(disposition, WindowOpenDisposition::CURRENT_TAB); |
| |
| EXPECT_CALL(*client(), OnAutocompleteAccept(_, _, _, _, _, _, _, _, _, _, _)) |
| .WillOnce(SaveArg<2>(&disposition)); |
| |
| match.provider = controller()->autocomplete_controller()->search_provider(); |
| match.from_keyword = true; |
| model()->OpenMatchForTesting(match, WindowOpenDisposition::CURRENT_TAB, |
| GURL(), std::u16string(), 0); |
| EXPECT_EQ(disposition, WindowOpenDisposition::CURRENT_TAB); |
| } |
| #endif // !(BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)) |
| |
| TEST_F(OmniboxEditModelTest, LogAnswerUsed) { |
| base::HistogramTester histogram_tester; |
| AutocompleteMatch match( |
| controller()->autocomplete_controller()->search_provider(), 0, false, |
| AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED); |
| match.answer_type = omnibox::ANSWER_TYPE_WEATHER; |
| match.destination_url = GURL("https://foo"); |
| model()->OpenMatchForTesting(match, WindowOpenDisposition::CURRENT_TAB, |
| GURL(), std::u16string(), 0); |
| histogram_tester.ExpectUniqueSample("Omnibox.SuggestionUsed.AnswerInSuggest", |
| 8, 1); |
| } |
| |
| // Tests `GetPopupRichSuggestionBitmap()` method, verifying that no bitmap is |
| // fetched when there is no match with an `associated_keyword`. |
| TEST_F(OmniboxEditModelPopupTest, |
| GetPopupRichSuggestionBitmapForMatchWithoutAssociatedKeyword) { |
| // Setup match with no bitmap. |
| ACMatches matches; |
| AutocompleteMatch match_without_associated_keyword( |
| nullptr, 1000, false, AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match_without_associated_keyword.keyword = |
| u"match_without_associated_keyword"; |
| matches.push_back(match_without_associated_keyword); |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| const SkBitmap* actual_bitmap = model()->GetPopupRichSuggestionBitmap( |
| u"match_without_associated_keyword"); |
| |
| EXPECT_FALSE(actual_bitmap); |
| } |
| |
| // Tests `GetPopupRichSuggestionBitmap()` method, verifying that the correct |
| // bitmap is fetched when there is a match with an `associated_keyword`. |
| TEST_F(OmniboxEditModelPopupTest, |
| GetPopupRichSuggestionBitmapForMatchWithAssociatedKeyword) { |
| SkBitmap expected_bitmap; |
| expected_bitmap.allocN32Pixels(16, 16); |
| expected_bitmap.eraseColor(SK_ColorRED); |
| |
| // Setup matches and add to result. |
| ACMatches matches; |
| AutocompleteMatch match_without_bitmap( |
| nullptr, 1000, false, AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match_without_bitmap.keyword = u"match_without_bitmap"; |
| match_without_bitmap.associated_keyword = |
| std::make_unique<AutocompleteMatch>(match_without_bitmap); |
| matches.push_back(match_without_bitmap); |
| AutocompleteMatch match_with_bitmap( |
| nullptr, 1000, false, AutocompleteMatchType::URL_WHAT_YOU_TYPED); |
| match_with_bitmap.keyword = u"match_with_bitmap"; |
| match_with_bitmap.associated_keyword = |
| std::make_unique<AutocompleteMatch>(match_with_bitmap); |
| matches.push_back(match_with_bitmap); |
| auto* result = &controller()->autocomplete_controller()->published_result_; |
| result->AppendMatches(matches); |
| |
| // Store bitmap for 'match_with_bitmap' match. |
| model()->rich_suggestion_bitmaps_.insert({1, expected_bitmap}); |
| |
| const SkBitmap* match_without_bitmap_bitmap = |
| model()->GetPopupRichSuggestionBitmap(u"match_without_bitmap"); |
| EXPECT_FALSE(match_without_bitmap_bitmap); |
| |
| const SkBitmap* match_with_bitmap_bitmap = |
| model()->GetPopupRichSuggestionBitmap(u"match_with_bitmap"); |
| EXPECT_TRUE(match_with_bitmap_bitmap); |
| gfx::test::CheckColors(expected_bitmap.getColor(0, 0), |
| match_with_bitmap_bitmap->getColor(0, 0)); |
| } |