blob: 071662c98186b63024c92a0337361075800d36ab [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/commander/commander_controller.h"
#include "base/callback.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/string16.h"
#include "chrome/browser/ui/commander/command_source.h"
#include "chrome/browser/ui/commander/commander_view_model.h"
#include "chrome/test/base/browser_with_test_window_test.h"
namespace commander {
namespace {
class TestCommandSource : public CommandSource {
public:
using GetCommandsHandler =
base::RepeatingCallback<CommandResults(const base::string16&,
Browser* browser)>;
explicit TestCommandSource(GetCommandsHandler handler)
: handler_(std::move(handler)) {}
~TestCommandSource() override = default;
CommandResults GetCommands(const base::string16& input,
Browser* browser) const override {
invocations_.push_back(input);
return handler_.Run(input, browser);
}
const std::vector<base::string16>& invocations() const {
return invocations_;
}
private:
mutable std::vector<base::string16> invocations_;
GetCommandsHandler handler_;
};
std::unique_ptr<TestCommandSource> CreateNoOpCommandSource() {
return std::make_unique<TestCommandSource>(base::BindRepeating(
[](const base::string16&,
Browser* browser) -> CommandSource::CommandResults { return {}; }));
}
std::unique_ptr<CommandItem> CreateNoOpCommandItem(const base::string16& title,
double score) {
auto item = std::make_unique<CommandItem>();
item->title = title;
item->score = score;
item->matched_ranges.emplace_back(0, title.size());
item->command = base::BindOnce([]() {});
return item;
}
template <typename T>
std::unique_ptr<CommandItem> CreateCompositeCommandItem(
const base::string16& title,
double scope) {}
TestCommandSource* AddSource(
std::vector<std::unique_ptr<CommandSource>>* sources,
std::unique_ptr<TestCommandSource> source) {
TestCommandSource* bare_ptr = source.get();
sources->push_back(std::move(source));
return bare_ptr;
}
} // namespace
class CommanderControllerTest : public BrowserWithTestWindowTest {
public:
class TestBackend : public CommanderBackend {
public:
explicit TestBackend(CommanderControllerTest* owner) {
owner->SetTestBackend(this);
}
void OnTextChanged(const base::string16& text, Browser* browser) override {
text_changed_invocations_.push_back(text);
}
void OnCommandSelected(size_t command_index, int result_set_id) override {
command_selected_invocations_.push_back(command_index);
}
void SetUpdateCallback(ViewModelUpdateCallback callback) override {
callback_ = std::move(callback);
}
void CallCallback() {
CommanderViewModel vm;
callback_.Run(vm);
}
const std::vector<base::string16> text_changed_invocations() {
return text_changed_invocations_;
}
const std::vector<size_t> command_selected_invocations() {
return command_selected_invocations_;
}
private:
ViewModelUpdateCallback callback_;
std::vector<base::string16> text_changed_invocations_;
std::vector<size_t> command_selected_invocations_;
};
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
expected_count_ = 0;
}
void ExpectViewModelCallbackCalls(int expected_count) {
expected_count_ += expected_count;
}
void WaitForExpectedCallbacks() {
if (expected_count_ <= 0)
return;
if (!run_loop_.get() || !run_loop_->running()) {
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
}
}
void OnViewModelUpdated(CommanderViewModel view_model) {
received_view_models_.push_back(view_model);
if (expected_count_ > 0) {
expected_count_--;
if (run_loop_.get() && expected_count_ == 0)
run_loop_->Quit();
}
}
void SetTestBackend(TestBackend* test_backend) {
test_backend_ = test_backend;
}
protected:
std::unique_ptr<base::RunLoop> run_loop_;
int expected_count_;
std::vector<CommanderViewModel> received_view_models_;
TestBackend* test_backend_;
};
class ViewModelCallbackWaiter {
public:
explicit ViewModelCallbackWaiter(CommanderControllerTest* test, int count = 1)
: test_(test) {
test_->ExpectViewModelCallbackCalls(count);
}
~ViewModelCallbackWaiter() { test_->WaitForExpectedCallbacks(); }
private:
CommanderControllerTest* test_;
};
TEST_F(CommanderControllerTest, PassesInputToCommandSourcesOnTextChanged) {
std::vector<std::unique_ptr<CommandSource>> sources;
TestCommandSource* first = AddSource(&sources, CreateNoOpCommandSource());
TestCommandSource* second = AddSource(&sources, CreateNoOpCommandSource());
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
EXPECT_EQ(first->invocations().size(), 0u);
EXPECT_EQ(second->invocations().size(), 0u);
base::string16 input = base::ASCIIToUTF16("foobar");
controller->OnTextChanged(input, browser());
EXPECT_EQ(first->invocations().size(), 1u);
EXPECT_EQ(second->invocations().size(), 1u);
EXPECT_EQ(first->invocations().back(), input);
EXPECT_EQ(second->invocations().back(), input);
}
TEST_F(CommanderControllerTest, ResultSetIdsDifferAcrossCalls) {
std::vector<std::unique_ptr<CommandSource>> sources;
ignore_result(AddSource(&sources, CreateNoOpCommandSource()));
base::RunLoop run_loop;
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("foobar"), browser());
}
// Assert since we're accessing an element.
ASSERT_EQ(received_view_models_.size(), 1u);
int first_id = received_view_models_.back().result_set_id;
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("barfoo"), browser());
}
EXPECT_EQ(received_view_models_.size(), 2u);
EXPECT_NE(received_view_models_.back().result_set_id, first_id);
}
TEST_F(CommanderControllerTest, ViewModelAggregatesResults) {
std::vector<std::unique_ptr<CommandSource>> sources;
auto first = std::make_unique<TestCommandSource>(
base::BindRepeating([](const base::string16&, Browser* browser) {
CommandSource::CommandResults result;
result.push_back(
CreateNoOpCommandItem(base::ASCIIToUTF16("first"), 100));
return result;
}));
auto second = std::make_unique<TestCommandSource>(
base::BindRepeating([](const base::string16&, Browser* browser) {
CommandSource::CommandResults result;
auto item = CreateNoOpCommandItem(base::ASCIIToUTF16("second"), 99);
item->annotation = base::ASCIIToUTF16("2nd");
item->entity_type = CommandItem::Entity::kBookmark;
result.push_back(std::move(item));
return result;
}));
sources.push_back(std::move(first));
sources.push_back(std::move(second));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("foobar"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
CommanderViewModel model = received_view_models_.back();
ASSERT_EQ(model.items.size(), 2u);
EXPECT_EQ(model.items[0].title, base::ASCIIToUTF16("first"));
EXPECT_EQ(model.items[0].annotation, base::string16());
EXPECT_EQ(model.items[0].entity_type, CommandItem::Entity::kCommand);
EXPECT_EQ(model.items[1].title, base::ASCIIToUTF16("second"));
EXPECT_EQ(model.items[1].annotation, base::ASCIIToUTF16("2nd"));
EXPECT_EQ(model.items[1].entity_type, CommandItem::Entity::kBookmark);
}
// TODO(lgrey): This will need to change when scoring gets more sophisticated
// than a simple sort.
TEST_F(CommanderControllerTest, ViewModelSortsResults) {
std::vector<std::unique_ptr<CommandSource>> sources;
auto first = std::make_unique<TestCommandSource>(
base::BindRepeating([](const base::string16&, Browser* browser) {
CommandSource::CommandResults result;
result.push_back(
CreateNoOpCommandItem(base::ASCIIToUTF16("third"), 98));
result.push_back(
CreateNoOpCommandItem(base::ASCIIToUTF16("first"), 100));
result.push_back(
CreateNoOpCommandItem(base::ASCIIToUTF16("fourth"), 90));
return result;
}));
auto second = std::make_unique<TestCommandSource>(
base::BindRepeating([](const base::string16&, Browser* browser) {
CommandSource::CommandResults result;
result.push_back(
CreateNoOpCommandItem(base::ASCIIToUTF16("second"), 99));
result.push_back(CreateNoOpCommandItem(base::ASCIIToUTF16("fifth"), 1));
return result;
}));
sources.push_back(std::move(first));
sources.push_back(std::move(second));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("foobar"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
CommanderViewModel model = received_view_models_.back();
ASSERT_EQ(model.items.size(), 5u);
EXPECT_EQ(model.items[0].title, base::ASCIIToUTF16("first"));
EXPECT_EQ(model.items[1].title, base::ASCIIToUTF16("second"));
EXPECT_EQ(model.items[2].title, base::ASCIIToUTF16("third"));
EXPECT_EQ(model.items[3].title, base::ASCIIToUTF16("fourth"));
EXPECT_EQ(model.items[4].title, base::ASCIIToUTF16("fifth"));
}
TEST_F(CommanderControllerTest, ViewModelRetainsBoldRanges) {
std::vector<std::unique_ptr<CommandSource>> sources;
auto source = std::make_unique<TestCommandSource>(
base::BindRepeating([=](const base::string16&, Browser* browser) {
auto first = CreateNoOpCommandItem(base::ASCIIToUTF16("first"), 100);
auto second = CreateNoOpCommandItem(base::ASCIIToUTF16("second"), 99);
first->matched_ranges.clear();
first->matched_ranges.emplace_back(0, 2);
first->matched_ranges.emplace_back(4, 1);
second->matched_ranges.clear();
second->matched_ranges.emplace_back(1, 4);
CommandSource::CommandResults result;
result.push_back(std::move(first));
result.push_back(std::move(second));
return result;
}));
sources.push_back(std::move(source));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("foobar"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
CommanderViewModel model = received_view_models_.back();
// Ensure |first| is at index 0;
EXPECT_EQ(model.items[0].title, base::ASCIIToUTF16("first"));
std::vector<gfx::Range> first_ranges = {gfx::Range(0, 2), gfx::Range(4, 1)};
std::vector<gfx::Range> second_ranges = {gfx::Range(1, 4)};
EXPECT_EQ(model.items[0].matched_ranges, first_ranges);
EXPECT_EQ(model.items[1].matched_ranges, second_ranges);
}
TEST_F(CommanderControllerTest, OnCommandSelectedInvokesOneShotCommand) {
std::vector<std::unique_ptr<CommandSource>> sources;
bool first_called = false;
bool second_called = false;
auto source = std::make_unique<TestCommandSource>(base::BindRepeating(
[](bool* first_called_ptr, bool* second_called_ptr, const base::string16&,
Browser* browser) {
auto first = CreateNoOpCommandItem(base::ASCIIToUTF16("first"), 100);
auto second = CreateNoOpCommandItem(base::ASCIIToUTF16("second"), 99);
first->command = base::BindOnce([](bool* called) { *called = true; },
first_called_ptr);
second->command = base::BindOnce([](bool* called) { *called = true; },
second_called_ptr);
CommandSource::CommandResults result;
result.push_back(std::move(first));
result.push_back(std::move(second));
return result;
},
&first_called, &second_called));
sources.push_back(std::move(source));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("foobar"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
CommanderViewModel model = received_view_models_.back();
// Ensure |first| is at index 0;
EXPECT_EQ(model.items[0].title, base::ASCIIToUTF16("first"));
{
ViewModelCallbackWaiter waiter(this);
controller->OnCommandSelected(0, model.result_set_id);
}
EXPECT_TRUE(first_called);
EXPECT_FALSE(second_called);
EXPECT_EQ(received_view_models_.size(), 2u);
EXPECT_EQ(received_view_models_.back().action,
CommanderViewModel::Action::kClose);
}
TEST_F(CommanderControllerTest, NoActionOnIncorrectResultId) {
std::vector<std::unique_ptr<CommandSource>> sources;
bool item_called = false;
auto source = std::make_unique<TestCommandSource>(base::BindRepeating(
[](bool* called_ptr, const base::string16&, Browser* browser) {
auto item = CreateNoOpCommandItem(base::ASCIIToUTF16("first"), 100);
item->command =
base::BindOnce([](bool* called) { *called = true; }, called_ptr);
CommandSource::CommandResults result;
result.push_back(std::move(item));
return result;
},
&item_called));
sources.push_back(std::move(source));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("foobar"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
CommanderViewModel model = received_view_models_.back();
controller->OnCommandSelected(0, model.result_set_id - 1);
EXPECT_FALSE(item_called);
}
TEST_F(CommanderControllerTest, NoActionOnOOBIndex) {
std::vector<std::unique_ptr<CommandSource>> sources;
bool item_called = false;
auto source = std::make_unique<TestCommandSource>(base::BindRepeating(
[](bool* called_ptr, const base::string16&, Browser* browser) {
auto item = CreateNoOpCommandItem(base::ASCIIToUTF16("first"), 100);
item->command =
base::BindOnce([](bool* called) { *called = true; }, called_ptr);
CommandSource::CommandResults result;
result.push_back(std::move(item));
return result;
},
&item_called));
sources.push_back(std::move(source));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("foobar"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
CommanderViewModel model = received_view_models_.back();
controller->OnCommandSelected(1, model.result_set_id);
EXPECT_FALSE(item_called);
}
TEST_F(CommanderControllerTest, InvokingCompositeCommandSendsPrompt) {
auto source = std::make_unique<TestCommandSource>(base::BindRepeating(
[](CommanderControllerTest* instance, const base::string16&,
Browser* browser) -> CommandSource::CommandResults {
auto item = std::make_unique<CommandItem>();
item->title = base::ASCIIToUTF16("Do something...");
item->score = 100;
item->matched_ranges.emplace_back(0, item->title.size());
item->delegate_factory = base::BindOnce(
[](CommanderControllerTest* instance)
-> std::unique_ptr<CommanderBackend> {
return std::make_unique<TestBackend>(instance);
},
instance);
CommandSource::CommandResults results;
results.push_back(std::move(item));
return results;
},
this));
std::vector<std::unique_ptr<CommandSource>> sources;
sources.push_back(std::move(source));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("abracadabra"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
{
ViewModelCallbackWaiter waiter(this);
controller->OnCommandSelected(0,
received_view_models_.back().result_set_id);
}
EXPECT_EQ(received_view_models_.back().action,
CommanderViewModel::Action::kPrompt);
}
TEST_F(CommanderControllerTest, OnTextChangedPassedToDelegate) {
auto source = std::make_unique<TestCommandSource>(base::BindRepeating(
[](CommanderControllerTest* instance, const base::string16&,
Browser* browser) -> CommandSource::CommandResults {
auto item = std::make_unique<CommandItem>();
item->title = base::ASCIIToUTF16("Do something...");
item->score = 100;
item->matched_ranges.emplace_back(0, item->title.size());
item->delegate_factory = base::BindOnce(
[](CommanderControllerTest* instance)
-> std::unique_ptr<CommanderBackend> {
return std::make_unique<TestBackend>(instance);
},
instance);
CommandSource::CommandResults results;
results.push_back(std::move(item));
return results;
},
this));
std::vector<std::unique_ptr<CommandSource>> sources;
sources.push_back(std::move(source));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("abracadabra"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
{
ViewModelCallbackWaiter waiter(this);
controller->OnCommandSelected(0,
received_view_models_.back().result_set_id);
}
EXPECT_TRUE(test_backend_->text_changed_invocations().empty());
controller->OnTextChanged(base::ASCIIToUTF16("hocus pocus"), browser());
ASSERT_EQ(test_backend_->text_changed_invocations().size(), 1u);
EXPECT_EQ(test_backend_->text_changed_invocations().back(),
base::ASCIIToUTF16("hocus pocus"));
}
TEST_F(CommanderControllerTest, OnCommandSelectedPassedToDelegate) {
auto source = std::make_unique<TestCommandSource>(base::BindRepeating(
[](CommanderControllerTest* instance, const base::string16&,
Browser* browser) -> CommandSource::CommandResults {
auto item = std::make_unique<CommandItem>();
item->title = base::ASCIIToUTF16("Do something...");
item->score = 100;
item->matched_ranges.emplace_back(0, item->title.size());
item->delegate_factory = base::BindOnce(
[](CommanderControllerTest* instance)
-> std::unique_ptr<CommanderBackend> {
return std::make_unique<TestBackend>(instance);
},
instance);
CommandSource::CommandResults results;
results.push_back(std::move(item));
return results;
},
this));
std::vector<std::unique_ptr<CommandSource>> sources;
sources.push_back(std::move(source));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(base::ASCIIToUTF16("abracadabra"), browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
{
ViewModelCallbackWaiter waiter(this);
controller->OnCommandSelected(0,
received_view_models_.back().result_set_id);
}
EXPECT_TRUE(test_backend_->text_changed_invocations().empty());
controller->OnCommandSelected(586,
received_view_models_.back().result_set_id);
ASSERT_EQ(test_backend_->command_selected_invocations().size(), 1u);
EXPECT_EQ(test_backend_->command_selected_invocations().back(), 586u);
}
} // namespace commander