blob: 69b73b50f412a152f259d87bf0cfd78678a55b02 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/omnibox/model/omnibox_autocomplete_controller.h"
#import "base/functional/callback.h"
#import "base/run_loop.h"
#import "base/test/task_environment.h"
#import "base/time/time.h"
#import "components/omnibox/browser/autocomplete_classifier.h"
#import "components/omnibox/browser/autocomplete_controller.h"
#import "components/omnibox/browser/autocomplete_match.h"
#import "components/omnibox/browser/autocomplete_match_test_util.h"
#import "components/omnibox/browser/autocomplete_result.h"
#import "components/omnibox/browser/fake_autocomplete_provider_client.h"
#import "components/omnibox/browser/omnibox_client.h"
#import "components/omnibox/browser/omnibox_controller.h"
#import "components/omnibox/browser/omnibox_edit_model.h"
#import "components/omnibox/browser/test_omnibox_client.h"
#import "components/open_from_clipboard/fake_clipboard_recent_content.h"
#import "components/prefs/testing_pref_service.h"
#import "ios/chrome/browser/omnibox/model/omnibox_autocomplete_controller_delegate.h"
#import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/test/testing_application_context.h"
#import "net/base/apple/url_conversions.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "ui/gfx/image/image.h"
#import "ui/gfx/image/image_skia.h"
#import "ui/gfx/image/image_unittest_util.h"
using testing::AtMost;
namespace {
// A mock class for the AutocompleteController.
class MockAutocompleteController : public AutocompleteController {
public:
MockAutocompleteController()
: AutocompleteController(
std::make_unique<FakeAutocompleteProviderClient>(),
AutocompleteClassifier::DefaultOmniboxProviders()) {}
MockAutocompleteController(const MockAutocompleteController&) = delete;
MockAutocompleteController& operator=(const MockAutocompleteController&) =
delete;
~MockAutocompleteController() override = default;
void SetAutocompleteMatches(const ACMatches& matches) {
AutocompleteResult& results =
const_cast<AutocompleteResult&>(this->result());
results.ClearMatches();
results.AppendMatches(matches);
}
void SetSteadyStateOmniboxPosition(
metrics::OmniboxEventProto::OmniboxPosition position) override {
omnibox_position = position;
}
MOCK_METHOD(void,
GroupSuggestionsBySearchVsURL,
(size_t begin, size_t end),
(override));
metrics::OmniboxEventProto::OmniboxPosition omnibox_position;
};
/// A mock class for OmniboxEditModel.
class MockOmniboxEditModel : public OmniboxEditModel {
public:
MockOmniboxEditModel(OmniboxController* controller)
: OmniboxEditModel(controller, nullptr),
last_opened_selection(OmniboxPopupSelection(UINT_MAX)) {}
MockOmniboxEditModel(const MockOmniboxEditModel&) = delete;
MockOmniboxEditModel& operator=(const MockOmniboxEditModel&) = delete;
~MockOmniboxEditModel() override = default;
void OpenSelection(OmniboxPopupSelection selection,
base::TimeTicks timestamp,
WindowOpenDisposition disposition) override {
last_opened_selection = selection;
if (open_selection_closure) {
open_selection_closure.Run();
open_selection_closure.Reset();
}
}
OmniboxPopupSelection last_opened_selection;
base::RepeatingClosure open_selection_closure;
};
} // namespace
class OmniboxAutocompleteControllerTest : public PlatformTest {
public:
OmniboxAutocompleteControllerTest() {
auto clipboard = std::make_unique<FakeClipboardRecentContent>();
clipboard_ = clipboard.get();
ClipboardRecentContent::SetInstance(std::move(clipboard));
local_state_ = std::make_unique<TestingPrefServiceSimple>();
RegisterLocalStatePrefs(local_state_->registry());
TestingApplicationContext::GetGlobal()->SetLocalState(local_state_.get());
auto omnibox_client = std::make_unique<TestOmniboxClient>();
omnibox_controller_ = std::make_unique<OmniboxController>(
/*view=*/nullptr, std::move(omnibox_client));
auto autocomplete = std::make_unique<MockAutocompleteController>();
autocomplete_controller_ = autocomplete.get();
omnibox_controller_->SetAutocompleteControllerForTesting(
std::move(autocomplete));
auto edit_model =
std::make_unique<MockOmniboxEditModel>(omnibox_controller_.get());
omnibox_edit_model_ = edit_model.get();
omnibox_controller_->SetEditModelForTesting(std::move(edit_model));
controller_delegate_ =
OCMProtocolMock(@protocol(OmniboxAutocompleteControllerDelegate));
controller_ = [[OmniboxAutocompleteController alloc]
initWithOmniboxController:omnibox_controller_.get()];
controller_.delegate = controller_delegate_;
}
~OmniboxAutocompleteControllerTest() override {
[controller_ disconnect];
clipboard_ = nullptr;
autocomplete_controller_ = nullptr;
omnibox_edit_model_ = nullptr;
omnibox_controller_ = nullptr;
controller_delegate_ = nil;
TestingApplicationContext::GetGlobal()->SetLocalState(nullptr);
local_state_.reset();
}
ACMatches SampleMatches() const {
return {CreateSearchMatch(u"Clear History"), CreateSearchMatch(u"search 1"),
CreateSearchMatch(u"search 2"),
CreateHistoryURLMatch(
/*destination_url=*/"http://this-site-matches.com")};
}
/// Returns the match opened by OmniboxEditModel::OpenSelection.
const AutocompleteMatch& LastOpenedMatch() {
return autocomplete_controller_->result().match_at(
omnibox_edit_model_->last_opened_selection.line);
}
protected:
// Message loop for the main test thread.
base::test::TaskEnvironment environment_;
// Application pref service.
std::unique_ptr<TestingPrefServiceSimple> local_state_;
OmniboxAutocompleteController* controller_;
raw_ptr<MockAutocompleteController> autocomplete_controller_;
raw_ptr<MockOmniboxEditModel> omnibox_edit_model_;
raw_ptr<FakeClipboardRecentContent> clipboard_;
std::unique_ptr<OmniboxController> omnibox_controller_;
id controller_delegate_;
};
// Custom matcher for AutocompleteMatch
MATCHER_P(IsSameAsMatch, expected, "") {
return arg.destination_url == expected.destination_url &&
arg.fill_into_edit == expected.fill_into_edit &&
arg.additional_text == expected.additional_text &&
arg.inline_autocompletion == expected.inline_autocompletion &&
arg.contents == expected.contents &&
arg.description == expected.description;
}
// Tests that adding fake matches adds them to the results.
TEST_F(OmniboxAutocompleteControllerTest, AddFakeMatches) {
ACMatches sample_matches = SampleMatches();
autocomplete_controller_->SetAutocompleteMatches(sample_matches);
EXPECT_EQ(autocomplete_controller_->result().size(), sample_matches.size());
}
#pragma mark - Request suggestion
// Tests requesting result when there are none still calls
// the delegate to update the suggestions groups.
TEST_F(OmniboxAutocompleteControllerTest, RequestResultEmpty) {
OCMExpect([controller_delegate_ omniboxAutocompleteController:[OCMArg any]
didUpdateSuggestionsGroups:[OCMArg any]]);
[controller_ requestSuggestionsWithVisibleSuggestionCount:0];
EXPECT_OCMOCK_VERIFY(controller_delegate_);
}
// Tests requesting result with all of them visible.
TEST_F(OmniboxAutocompleteControllerTest, RequestResultsAllVisible) {
autocomplete_controller_->SetAutocompleteMatches(SampleMatches());
// Expect one group of suggestions.
EXPECT_CALL(*autocomplete_controller_,
GroupSuggestionsBySearchVsURL(
1, autocomplete_controller_->result().size()));
OCMExpect([controller_delegate_ omniboxAutocompleteController:[OCMArg any]
didUpdateSuggestionsGroups:[OCMArg any]]);
// Request results with everything visible.
[controller_ requestSuggestionsWithVisibleSuggestionCount:0];
EXPECT_OCMOCK_VERIFY(controller_delegate_);
}
// Tests requesting result with more suggestions visible than available.
TEST_F(OmniboxAutocompleteControllerTest, RequestResultVisibleOverflow) {
autocomplete_controller_->SetAutocompleteMatches(SampleMatches());
// Expect one group of suggestions.
EXPECT_CALL(*autocomplete_controller_,
GroupSuggestionsBySearchVsURL(
1, autocomplete_controller_->result().size()));
OCMExpect([controller_delegate_ omniboxAutocompleteController:[OCMArg any]
didUpdateSuggestionsGroups:[OCMArg any]]);
// Request results with more visible than available.
[controller_ requestSuggestionsWithVisibleSuggestionCount:100];
EXPECT_OCMOCK_VERIFY(controller_delegate_);
}
// Tests requesting result with part of them visible.
TEST_F(OmniboxAutocompleteControllerTest, RequestResultPartVisible) {
autocomplete_controller_->SetAutocompleteMatches(SampleMatches());
size_t result_size = autocomplete_controller_->result().size();
size_t visible_count = 2;
EXPECT_LT(visible_count, result_size);
// Expect a first group of visible suggestions.
EXPECT_CALL(*autocomplete_controller_,
GroupSuggestionsBySearchVsURL(1, visible_count));
// Expect a second group of hidden suggestions.
EXPECT_CALL(*autocomplete_controller_,
GroupSuggestionsBySearchVsURL(visible_count, result_size));
OCMExpect([controller_delegate_ omniboxAutocompleteController:[OCMArg any]
didUpdateSuggestionsGroups:[OCMArg any]]);
// Request results with everything visible.
[controller_ requestSuggestionsWithVisibleSuggestionCount:visible_count];
EXPECT_OCMOCK_VERIFY(controller_delegate_);
}
#pragma mark - Logging
// Tests that omnibox position update is forwarded to autocompleteController.
TEST_F(OmniboxAutocompleteControllerTest, OmniboxPositionUpdates) {
local_state_->SetBoolean(prefs::kBottomOmnibox, true);
EXPECT_EQ(autocomplete_controller_->omnibox_position,
metrics::OmniboxEventProto::BOTTOM_POSITION);
local_state_->SetBoolean(prefs::kBottomOmnibox, false);
EXPECT_EQ(autocomplete_controller_->omnibox_position,
metrics::OmniboxEventProto::TOP_POSITION);
}
#pragma mark - Open match
// Tests opening a match that doesn't exist in autocomplete controller.
TEST_F(OmniboxAutocompleteControllerTest, OpenCreatedMatch) {
autocomplete_controller_->SetAutocompleteMatches(SampleMatches());
AutocompleteMatch match = CreateSearchMatch(u"some match");
// Open match that doesn't come from the autocomplete controller. Row is
// higher than autocomplete_controller_->result().size().
[controller_ selectMatchForOpening:match
inRow:10
openIn:WindowOpenDisposition::CURRENT_TAB];
// Expect the match to be opened.
EXPECT_THAT(LastOpenedMatch(), IsSameAsMatch(match));
// Reset the last opened selection.
omnibox_edit_model_->last_opened_selection = OmniboxPopupSelection(UINT_MAX);
// Open match that doesn't come from the autocomplete controller. Row is
// smaller than autocomplete_controller_->result().size().
[controller_ selectMatchForOpening:match
inRow:1
openIn:WindowOpenDisposition::CURRENT_TAB];
// Expect the match to be opened.
EXPECT_THAT(LastOpenedMatch(), IsSameAsMatch(match));
}
// Tests opening a clipboard URL match.
TEST_F(OmniboxAutocompleteControllerTest, OpenClipboardURLMatch) {
// Create an empty clipboard match in autocompleteController.
AutocompleteMatch clipboard_match = CreateAutocompleteMatch(
"Clipboard match", AutocompleteMatchType::CLIPBOARD_URL, false, false,
100, std::nullopt);
clipboard_match.destination_url = GURL();
autocomplete_controller_->SetAutocompleteMatches({clipboard_match});
// Set the clipboard content.
GURL pasteboard_url = GURL("https://chromium.org");
clipboard_->SetClipboardURL(pasteboard_url, base::TimeDelta::Min());
// Open the clipboard match.
[controller_ selectMatchForOpening:clipboard_match
inRow:0
openIn:WindowOpenDisposition::CURRENT_TAB];
// Expect the clipboard content to be loaded.
EXPECT_EQ(LastOpenedMatch().destination_url, pasteboard_url);
}
// Tests opening a clipboard Text match.
TEST_F(OmniboxAutocompleteControllerTest, OpenClipboardTextMatch) {
// Create an empty clipboard match in autocompleteController.
AutocompleteMatch clipboard_match = CreateAutocompleteMatch(
"Clipboard text match", AutocompleteMatchType::CLIPBOARD_TEXT, false,
false, 100, std::nullopt);
clipboard_match.destination_url = GURL();
autocomplete_controller_->SetAutocompleteMatches({clipboard_match});
// Set the clipboard content.
std::u16string pasteboard_text = u"search terms";
clipboard_->SetClipboardText(pasteboard_text, base::TimeDelta::Min());
// Open the clipboard match.
[controller_ selectMatchForOpening:clipboard_match
inRow:0
openIn:WindowOpenDisposition::CURRENT_TAB];
// Expect the clipboard content to be loaded.
EXPECT_EQ(LastOpenedMatch().fill_into_edit, pasteboard_text);
}
// Tests opening a clipboard Image match.
TEST_F(OmniboxAutocompleteControllerTest, OpenClipboardImageMatch) {
// Create an empty clipboard match in autocompleteController.
AutocompleteMatch clipboard_match = CreateAutocompleteMatch(
"Clipboard image match", AutocompleteMatchType::CLIPBOARD_IMAGE, false,
false, 100, std::nullopt);
clipboard_match.destination_url = GURL();
autocomplete_controller_->SetAutocompleteMatches({clipboard_match});
// Set the clipboard content.
gfx::Image pasteboard_image =
gfx::test::CreateImage(/*width=*/10, /*height=*/10);
clipboard_->SetClipboardImage(pasteboard_image, base::TimeDelta::Min());
// Setup the OpenSelection waiter.
base::RunLoop open_selection_waiter;
omnibox_edit_model_->open_selection_closure =
open_selection_waiter.QuitClosure();
// Open the clipboard match.
[controller_ selectMatchForOpening:clipboard_match
inRow:0
openIn:WindowOpenDisposition::CURRENT_TAB];
// Wait for the image match.
open_selection_waiter.Run();
// Expect the clipboard content to be loaded.
EXPECT_EQ(LastOpenedMatch().type, AutocompleteMatchType::CLIPBOARD_IMAGE);
EXPECT_FALSE(LastOpenedMatch().post_content->first.empty());
EXPECT_FALSE(LastOpenedMatch().post_content->second.empty());
}