blob: dda6b0505fcddb97a0ef0cc01512fd206624ccde [file] [log] [blame]
// Copyright 2017 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 "components/omnibox/browser/omnibox_edit_model.h"
#include <stddef.h>
#include <memory>
#include <string>
#include "base/stl_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/omnibox_field_trial.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_controller.h"
#include "components/omnibox/browser/test_omnibox_edit_model.h"
#include "components/omnibox/browser/test_omnibox_view.h"
#include "components/url_formatter/url_fixer.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"
using metrics::OmniboxEventProto;
class OmniboxEditModelTest : public testing::Test {
public:
void SetUp() override {
controller_ = std::make_unique<TestOmniboxEditController>();
view_ = std::make_unique<TestOmniboxView>(controller_.get());
view_->SetModel(
std::make_unique<TestOmniboxEditModel>(view_.get(), controller_.get()));
}
TestOmniboxView* view() { return view_.get(); }
TestLocationBarModel* location_bar_model() {
return controller_->GetLocationBarModel();
}
TestOmniboxEditModel* model() {
return static_cast<TestOmniboxEditModel*>(view_->model());
}
private:
base::test::TaskEnvironment task_environment_;
std::unique_ptr<TestOmniboxEditController> controller_;
std::unique_ptr<TestOmniboxView> view_;
};
// Tests various permutations of AutocompleteModel::AdjustTextForCopy.
TEST_F(OmniboxEditModelTest, AdjustTextForCopy) {
struct Data {
const char* url_for_editing;
const int sel_start;
const char* match_destination_url;
const bool is_match_selected_in_popup;
const char* input;
const char* expected_output;
const bool write_url;
const char* expected_url;
const char* url_for_display = "";
} input[] = {
// Test that http:// is inserted if all text is selected.
{"a.de/b", 0, "", false, "a.de/b", "http://a.de/b", true,
"http://a.de/b"},
// Test that http:// and https:// are inserted if the host is selected.
{"a.de/b", 0, "", false, "a.de/", "http://a.de/", true, "http://a.de/"},
{"https://a.de/b", 0, "", false, "https://a.de/", "https://a.de/", true,
"https://a.de/"},
// Tests that http:// is inserted if the path is modified.
{"a.de/b", 0, "", false, "a.de/c", "http://a.de/c", true,
"http://a.de/c"},
// Tests that http:// isn't inserted if the host is modified.
{"a.de/b", 0, "", false, "a.com/b", "a.com/b", false, ""},
// Tests that http:// isn't inserted if the start of the selection is 1.
{"a.de/b", 1, "", false, "a.de/b", "a.de/b", false, ""},
// Tests that http:// isn't inserted if a portion of the host is selected.
{"a.de/", 0, "", false, "a.d", "a.d", false, ""},
// Tests that http:// isn't inserted if the user adds to the host.
{"a.de/", 0, "", false, "a.de.com/", "a.de.com/", false, ""},
// Tests that we don't get double schemes if the user manually inserts
// a scheme.
{"a.de/", 0, "", false, "http://a.de/", "http://a.de/", true,
"http://a.de/"},
{"a.de/", 0, "", false, "HTtp://a.de/", "http://a.de/", true,
"http://a.de/"},
{"https://a.de/", 0, "", false, "https://a.de/", "https://a.de/", true,
"https://a.de/"},
// Test that we don't get double schemes or revert the change if the user
// manually changes the scheme from 'http://' to 'https://' or vice versa.
{"a.de/", 0, "", false, "https://a.de/", "https://a.de/", true,
"https://a.de/"},
{"https://a.de/", 0, "", false, "http://a.de/", "http://a.de/", true,
"http://a.de/"},
// Makes sure intranet urls get 'http://' prefixed to them.
{"b/foo", 0, "", false, "b/foo", "http://b/foo", true, "http://b/foo",
"b/foo"},
// Verifies a search term 'foo' doesn't end up with http.
{"www.google.com/search?", 0, "", false, "foo", "foo", false, ""},
// Verifies that http:// and https:// are inserted for a match in a popup.
{"a.com", 0, "http://b.com/foo", true, "b.com/foo", "http://b.com/foo",
true, "http://b.com/foo"},
{"a.com", 0, "https://b.com/foo", true, "b.com/foo", "https://b.com/foo",
true, "https://b.com/foo"},
// Even if the popup is open, if the input text doesn't correspond to the
// current match, ignore the current match.
{"a.com/foo", 0, "https://b.com/foo", true, "a.com/foo", "a.com/foo",
false, "a.com/foo"},
{"https://b.com/foo", 0, "https://b.com/foo", true, "https://b.co",
"https://b.co", false, "https://b.co"},
// Verifies that no scheme is inserted if there is no valid match.
{"a.com", 0, "", true, "b.com/foo", "b.com/foo", false, ""},
// Steady State Elisions test for re-adding an elided 'https://'.
{"https://a.de/b", 0, "", false, "a.de/b", "https://a.de/b", true,
"https://a.de/b", "a.de/b"},
// Verifies that non-ASCII characters are %-escaped for valid copied URLs,
// as long as the host has not been modified from the page URL.
{u8"https://ja.wikipedia.org/wiki/目次", 0, "", false,
u8"https://ja.wikipedia.org/wiki/目次",
"https://ja.wikipedia.org/wiki/%E7%9B%AE%E6%AC%A1", true,
"https://ja.wikipedia.org/wiki/%E7%9B%AE%E6%AC%A1"},
// Test escaping when part of the path was not copied.
{u8"https://ja.wikipedia.org/wiki/目次", 0, "", false,
u8"https://ja.wikipedia.org/wiki/目",
"https://ja.wikipedia.org/wiki/%E7%9B%AE", true,
"https://ja.wikipedia.org/wiki/%E7%9B%AE"},
// Correctly handle escaping in the scheme-elided case as well.
{u8"https://ja.wikipedia.org/wiki/目次", 0, "", false,
u8"ja.wikipedia.org/wiki/目次",
"https://ja.wikipedia.org/wiki/%E7%9B%AE%E6%AC%A1", true,
"https://ja.wikipedia.org/wiki/%E7%9B%AE%E6%AC%A1",
u8"ja.wikipedia.org/wiki/目次"},
// Don't escape when host was modified.
{u8"https://ja.wikipedia.org/wiki/目次", 0, "", false,
u8"https://wikipedia.org/wiki/目次", u8"https://wikipedia.org/wiki/目次",
false, ""},
};
for (size_t i = 0; i < base::size(input); ++i) {
location_bar_model()->set_formatted_full_url(
base::UTF8ToUTF16(input[i].url_for_editing));
location_bar_model()->set_url_for_display(
base::UTF8ToUTF16(input[i].url_for_display));
// Set the location bar model's URL to be a valid GURL that would generate
// the test case's url_for_editing.
location_bar_model()->set_url(
url_formatter::FixupURL(input[i].url_for_editing, ""));
model()->ResetDisplayTexts();
model()->SetInputInProgress(input[i].is_match_selected_in_popup);
model()->SetPopupIsOpen(input[i].is_match_selected_in_popup);
AutocompleteMatch match;
match.type = AutocompleteMatchType::NAVSUGGEST;
match.destination_url = GURL(input[i].match_destination_url);
model()->SetCurrentMatchForTest(match);
base::string16 result = base::UTF8ToUTF16(input[i].input);
GURL url;
bool write_url;
model()->AdjustTextForCopy(input[i].sel_start, &result, &url, &write_url);
EXPECT_EQ(base::UTF8ToUTF16(input[i].expected_output), result)
<< "@: " << i;
EXPECT_EQ(input[i].write_url, write_url) << " @" << i;
if (write_url)
EXPECT_EQ(input[i].expected_url, url.spec()) << " @" << i;
}
}
// Tests that AdjustTextForCopy behaves properly with Query in Omnibox enabled.
// For more general tests of copy adjustment, see the AdjustTextForCopy test.
TEST_F(OmniboxEditModelTest, AdjustTextForCopyQueryInOmnibox) {
location_bar_model()->set_url(GURL("https://www.example.com/"));
location_bar_model()->set_url_for_display(base::ASCIIToUTF16("example.com"));
location_bar_model()->set_display_search_terms(base::ASCIIToUTF16("foobar"));
model()->ResetDisplayTexts();
// Verify that we copy the query verbatim when nothing has been modified.
{
base::string16 result = base::ASCIIToUTF16("foobar");
GURL url;
bool write_url = false;
model()->AdjustTextForCopy(0, &result, &url, &write_url);
EXPECT_EQ(base::ASCIIToUTF16("foobar"), result);
EXPECT_EQ(GURL("https://www.example.com/"), url);
EXPECT_TRUE(write_url);
}
// Verify we copy the query verbatim even if the user has refined the query.
{
base::string16 result = base::ASCIIToUTF16("something else");
GURL url;
bool write_url = false;
model()->AdjustTextForCopy(0, &result, &url, &write_url);
EXPECT_EQ(base::ASCIIToUTF16("something else"), result);
EXPECT_EQ(GURL(), url);
EXPECT_FALSE(write_url);
}
}
TEST_F(OmniboxEditModelTest, InlineAutocompleteText) {
// Test if the model updates the inline autocomplete text in the view.
EXPECT_EQ(base::string16(), view()->inline_autocomplete_text());
model()->SetUserText(base::ASCIIToUTF16("he"));
model()->OnPopupDataChanged(base::ASCIIToUTF16("llo"), nullptr,
base::string16(), false);
EXPECT_EQ(base::ASCIIToUTF16("hello"), view()->GetText());
EXPECT_EQ(base::ASCIIToUTF16("llo"), view()->inline_autocomplete_text());
base::string16 text_before = base::ASCIIToUTF16("he");
base::string16 text_after = base::ASCIIToUTF16("hel");
OmniboxView::StateChanges state_changes{
&text_before, &text_after, 3, 3, false, true, false, false};
model()->OnAfterPossibleChange(state_changes, true);
EXPECT_EQ(base::string16(), view()->inline_autocomplete_text());
model()->OnPopupDataChanged(base::ASCIIToUTF16("lo"), nullptr,
base::string16(), false);
EXPECT_EQ(base::ASCIIToUTF16("hello"), view()->GetText());
EXPECT_EQ(base::ASCIIToUTF16("lo"), view()->inline_autocomplete_text());
model()->Revert();
EXPECT_EQ(base::string16(), view()->GetText());
EXPECT_EQ(base::string16(), view()->inline_autocomplete_text());
model()->SetUserText(base::ASCIIToUTF16("he"));
model()->OnPopupDataChanged(base::ASCIIToUTF16("llo"), nullptr,
base::string16(), false);
EXPECT_EQ(base::ASCIIToUTF16("hello"), view()->GetText());
EXPECT_EQ(base::ASCIIToUTF16("llo"), view()->inline_autocomplete_text());
model()->AcceptTemporaryTextAsUserText();
EXPECT_EQ(base::ASCIIToUTF16("hello"), view()->GetText());
EXPECT_EQ(base::string16(), view()->inline_autocomplete_text());
}
// iOS doesn't use elisions in the Omnibox textfield.
#if !defined(OS_IOS)
TEST_F(OmniboxEditModelTest, RespectUnelisionInZeroSuggest) {
location_bar_model()->set_url(GURL("https://www.example.com/"));
location_bar_model()->set_url_for_display(base::ASCIIToUTF16("example.com"));
EXPECT_TRUE(model()->ResetDisplayTexts());
model()->Revert();
// Set up view with unelided text.
EXPECT_EQ(base::ASCIIToUTF16("example.com"), view()->GetText());
EXPECT_TRUE(model()->Unelide(false /* exit_query_in_omnibox */));
EXPECT_EQ(base::ASCIIToUTF16("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(base::string16(), view()->inline_autocomplete_text());
model()->OnPopupDataChanged(base::string16(), nullptr, base::string16(),
false);
EXPECT_EQ(base::ASCIIToUTF16("https://www.example.com/"), view()->GetText());
EXPECT_FALSE(model()->user_input_in_progress());
EXPECT_TRUE(view()->IsSelectAll());
}
#endif // !defined(OS_IOS)
// 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) {
const TestOmniboxClient* client =
static_cast<TestOmniboxClient*>(model()->client());
const AutocompleteMatch match(
model()->autocomplete_controller()->search_provider(), 0, false,
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED);
const GURL alternate_nav_url("http://abcd/");
model()->OnSetFocus(false); // Avoids DCHECK in OpenMatch().
model()->SetUserText(base::ASCIIToUTF16("http://abcd"));
model()->OpenMatch(match, WindowOpenDisposition::CURRENT_TAB,
alternate_nav_url, base::string16(), 0);
EXPECT_TRUE(AutocompleteInput::HasHTTPScheme(
client->alternate_nav_match().fill_into_edit));
model()->SetUserText(base::ASCIIToUTF16("abcd"));
model()->OpenMatch(match, WindowOpenDisposition::CURRENT_TAB,
alternate_nav_url, base::string16(), 0);
EXPECT_TRUE(AutocompleteInput::HasHTTPScheme(
client->alternate_nav_match().fill_into_edit));
}
TEST_F(OmniboxEditModelTest, CurrentMatch) {
location_bar_model()->set_url(GURL("http://localhost/"));
location_bar_model()->set_url_for_display(base::ASCIIToUTF16("localhost"));
model()->ResetDisplayTexts();
model()->Revert();
// Tests that we use the formatted full URL instead of the elided URL to
// generate matches.
{
AutocompleteMatch match = model()->CurrentMatch(nullptr);
EXPECT_EQ(AutocompleteMatchType::URL_WHAT_YOU_TYPED, match.type);
EXPECT_TRUE(model()->CurrentTextIsURL());
}
// Tests that when there is a Query in Omnibox, generate matches from the
// query, instead of the full formatted URL.
location_bar_model()->set_display_search_terms(base::ASCIIToUTF16("foobar"));
model()->ResetDisplayTexts();
model()->Revert();
{
AutocompleteMatch match = model()->CurrentMatch(nullptr);
EXPECT_EQ(AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED, match.type);
EXPECT_FALSE(model()->CurrentTextIsURL());
}
}
TEST_F(OmniboxEditModelTest, DisplayText) {
location_bar_model()->set_url(GURL("https://www.example.com/"));
location_bar_model()->set_url_for_display(base::ASCIIToUTF16("example.com"));
EXPECT_TRUE(model()->ResetDisplayTexts());
model()->Revert();
EXPECT_TRUE(model()->CurrentTextIsURL());
#if defined(OS_IOS)
// iOS OmniboxEditModel always provides the full URL as the OmniboxView
// permanent display text. Unelision should return false.
EXPECT_EQ(base::ASCIIToUTF16("https://www.example.com/"),
model()->GetPermanentDisplayText());
EXPECT_EQ(base::ASCIIToUTF16("https://www.example.com/"), view()->GetText());
EXPECT_FALSE(model()->Unelide(false /* exit_query_in_omnibox */));
EXPECT_FALSE(model()->user_input_in_progress());
EXPECT_FALSE(view()->IsSelectAll());
#else
// Verify we can unelide and show the full URL properly.
EXPECT_EQ(base::ASCIIToUTF16("example.com"),
model()->GetPermanentDisplayText());
EXPECT_EQ(base::ASCIIToUTF16("example.com"), view()->GetText());
EXPECT_TRUE(model()->Unelide(false /* exit_query_in_omnibox */));
EXPECT_FALSE(model()->user_input_in_progress());
EXPECT_TRUE(view()->IsSelectAll());
#endif
EXPECT_EQ(base::ASCIIToUTF16("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(base::ASCIIToUTF16("something else"));
EXPECT_FALSE(model()->ShouldShowCurrentPageIcon());
}
TEST_F(OmniboxEditModelTest, DisplayAndExitQueryInOmnibox) {
location_bar_model()->set_url(GURL("https://www.example.com/"));
location_bar_model()->set_url_for_display(base::ASCIIToUTF16("example.com"));
location_bar_model()->set_display_search_terms(base::ASCIIToUTF16("foobar"));
EXPECT_TRUE(model()->ResetDisplayTexts());
model()->Revert();
EXPECT_EQ(base::ASCIIToUTF16("foobar"), model()->GetPermanentDisplayText());
EXPECT_EQ(base::ASCIIToUTF16("foobar"), view()->GetText());
EXPECT_FALSE(model()->CurrentTextIsURL());
EXPECT_TRUE(model()->ShouldShowCurrentPageIcon());
// Verify we can exit Query in Omnibox mode properly.
EXPECT_TRUE(model()->Unelide(true /* exit_query_in_omnibox */));
EXPECT_EQ(base::ASCIIToUTF16("https://www.example.com/"), view()->GetText());
EXPECT_FALSE(model()->user_input_in_progress());
EXPECT_TRUE(view()->IsSelectAll());
EXPECT_TRUE(model()->CurrentTextIsURL());
// We should still show the current page's icon until the URL is modified.
EXPECT_TRUE(model()->ShouldShowCurrentPageIcon());
view()->SetUserText(base::ASCIIToUTF16("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(
base::ASCIIToUTF16("https://www.example.com/"));
EXPECT_TRUE(model()->ResetDisplayTexts());
model()->Revert();
EXPECT_EQ(base::ASCIIToUTF16("https://www.example.com/"),
model()->GetPermanentDisplayText());
EXPECT_TRUE(model()->CurrentTextIsURL());
// Verify Unelide does nothing.
EXPECT_FALSE(model()->Unelide(false /* exit_query_in_omnibox */));
EXPECT_EQ(base::ASCIIToUTF16("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) {
// 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());
}
// 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(base::UTF8ToUTF16("user"));
view()->OnInlineAutocompleteTextMaybeChanged(base::UTF8ToUTF16("user text"),
4);
// Entering keyword search mode should preserve the full display text as the
// user text, and select all.
model()->EnterKeywordModeForDefaultSearchProvider(
OmniboxEventProto::KEYBOARD_SHORTCUT);
EXPECT_EQ(base::UTF8ToUTF16("user text"), model()->GetUserTextForTesting());
EXPECT_EQ(base::UTF8ToUTF16("user text"), view()->GetText());
EXPECT_TRUE(view()->IsSelectAll());
// Deleting the user text (exiting keyword) mode should clear everything.
view()->SetUserText(base::string16());
{
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(base::UTF8ToUTF16("user text"));
GURL destination_url("http://example.com");
// OnPopupDataChanged() is called when the user focuses a suggestion.
model()->OnPopupDataChanged(base::UTF8ToUTF16("match text"), &destination_url,
base::string16(), false);
// Entering keyword search mode should preserve temporary text as the user
// text, and select all.
model()->EnterKeywordModeForDefaultSearchProvider(
OmniboxEventProto::KEYBOARD_SHORTCUT);
EXPECT_EQ(base::UTF8ToUTF16("match text"), model()->GetUserTextForTesting());
EXPECT_EQ(base::UTF8ToUTF16("match text"), view()->GetText());
EXPECT_TRUE(view()->IsSelectAll());
}