blob: 7ef7bef51e71d1f39660e036d630221548c7777c [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 <string>
#include <tuple>
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/bind.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 std::u16string&,
Browser* browser)>;
explicit TestCommandSource(GetCommandsHandler handler)
: handler_(std::move(handler)) {}
~TestCommandSource() override = default;
CommandResults GetCommands(const std::u16string& input,
Browser* browser) const override {
invocations_.push_back(input);
return handler_.Run(input, browser);
}
const std::vector<std::u16string>& invocations() const {
return invocations_;
}
private:
mutable std::vector<std::u16string> invocations_;
GetCommandsHandler handler_;
};
std::unique_ptr<TestCommandSource> CreateNoOpCommandSource() {
return std::make_unique<TestCommandSource>(base::BindRepeating(
[](const std::u16string&,
Browser* browser) -> CommandSource::CommandResults { return {}; }));
}
std::unique_ptr<CommandItem> CreateNoOpCommandItem(const std::u16string& title,
double score) {
std::vector<gfx::Range> ranges{{0, static_cast<uint32_t>(title.size())}};
auto item = std::make_unique<CommandItem>(title, score, ranges);
item->command = base::DoNothing();
return item;
}
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:
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();
}
}
protected:
std::unique_ptr<base::RunLoop> run_loop_;
int expected_count_;
std::vector<CommanderViewModel> received_view_models_;
};
class ViewModelCallbackWaiter {
public:
explicit ViewModelCallbackWaiter(CommanderControllerTest* test, int count = 1)
: test_(test) {
test_->ExpectViewModelCallbackCalls(count);
}
~ViewModelCallbackWaiter() { test_->WaitForExpectedCallbacks(); }
private:
raw_ptr<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);
std::u16string input = u"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;
std::ignore = 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(u"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(u"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 std::u16string&, Browser* browser) {
CommandSource::CommandResults result;
result.push_back(CreateNoOpCommandItem(u"first", 100));
return result;
}));
auto second = std::make_unique<TestCommandSource>(
base::BindRepeating([](const std::u16string&, Browser* browser) {
CommandSource::CommandResults result;
auto item = CreateNoOpCommandItem(u"second", 99);
item->annotation = u"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(u"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, u"first");
EXPECT_EQ(model.items[0].annotation, std::u16string());
EXPECT_EQ(model.items[0].entity_type, CommandItem::Entity::kCommand);
EXPECT_EQ(model.items[1].title, u"second");
EXPECT_EQ(model.items[1].annotation, u"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 std::u16string&, Browser* browser) {
CommandSource::CommandResults result;
result.push_back(CreateNoOpCommandItem(u"third", 98));
result.push_back(CreateNoOpCommandItem(u"first", 100));
result.push_back(CreateNoOpCommandItem(u"fourth", 90));
return result;
}));
auto second = std::make_unique<TestCommandSource>(
base::BindRepeating([](const std::u16string&, Browser* browser) {
CommandSource::CommandResults result;
result.push_back(CreateNoOpCommandItem(u"second", 99));
result.push_back(CreateNoOpCommandItem(u"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(u"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, u"first");
EXPECT_EQ(model.items[1].title, u"second");
EXPECT_EQ(model.items[2].title, u"third");
EXPECT_EQ(model.items[3].title, u"fourth");
EXPECT_EQ(model.items[4].title, u"fifth");
}
TEST_F(CommanderControllerTest, ViewModelSortsSameScoreAlphabetically) {
std::vector<std::unique_ptr<CommandSource>> sources;
auto source = std::make_unique<TestCommandSource>(
base::BindRepeating([](const std::u16string&, Browser* browser) {
CommandSource::CommandResults result;
result.push_back(CreateNoOpCommandItem(u"clementine", 100));
result.push_back(CreateNoOpCommandItem(u"apple", 100));
result.push_back(CreateNoOpCommandItem(u"banana", 100));
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(u"foobar", browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
CommanderViewModel model = received_view_models_.back();
ASSERT_EQ(model.items.size(), 3u);
EXPECT_EQ(model.items[0].title, u"apple");
EXPECT_EQ(model.items[1].title, u"banana");
EXPECT_EQ(model.items[2].title, u"clementine");
}
TEST_F(CommanderControllerTest, ViewModelRetainsBoldRanges) {
std::vector<std::unique_ptr<CommandSource>> sources;
auto source = std::make_unique<TestCommandSource>(
base::BindRepeating([=](const std::u16string&, Browser* browser) {
auto first = CreateNoOpCommandItem(u"first", 100);
auto second = CreateNoOpCommandItem(u"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(u"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, u"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 std::u16string&,
Browser* browser) {
auto first = CreateNoOpCommandItem(u"first", 100);
auto second = CreateNoOpCommandItem(u"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(u"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, u"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 std::u16string&, Browser* browser) {
auto item = CreateNoOpCommandItem(u"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(u"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 std::u16string&, Browser* browser) {
auto item = CreateNoOpCommandItem(u"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(u"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) {
std::vector<std::unique_ptr<CommandSource>> sources;
auto source = std::make_unique<TestCommandSource>(
base::BindRepeating([](const std::u16string&, Browser* browser) {
auto item = CreateNoOpCommandItem(u"first", 100);
CommandItem::CompositeCommandProvider noop =
base::BindRepeating([](const std::u16string&) {
return CommandSource::CommandResults();
});
item->command = std::make_pair(u"Do stuff", noop);
CommandSource::CommandResults result;
result.push_back(std::move(item));
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(u"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);
EXPECT_EQ(received_view_models_.back().prompt_text, u"Do stuff");
}
TEST_F(CommanderControllerTest, OnTextChangedPassedToCompositeCommandProvider) {
std::vector<std::unique_ptr<CommandSource>> sources;
std::u16string received_string;
auto source = std::make_unique<TestCommandSource>(base::BindRepeating(
[](std::u16string* passthrough_string, const std::u16string& string,
Browser* browser) {
auto item = CreateNoOpCommandItem(u"first", 100);
CommandItem::CompositeCommandProvider provider = base::BindRepeating(
[](std::u16string* out_string, const std::u16string& string) {
*out_string = string;
return CommandSource::CommandResults();
},
passthrough_string);
item->command = std::make_pair(u"Do stuff", provider);
CommandSource::CommandResults result;
result.push_back(std::move(item));
return result;
},
&received_string));
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(u"abracadabra", browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
{
ViewModelCallbackWaiter waiter(this);
controller->OnCommandSelected(0,
received_view_models_.back().result_set_id);
}
controller->OnTextChanged(u"hocus pocus", browser());
EXPECT_EQ(received_string, u"hocus pocus");
}
TEST_F(CommanderControllerTest,
CompositeProviderCommandsArePresentedAndExecuted) {
std::vector<std::unique_ptr<CommandSource>> sources;
auto source = std::make_unique<TestCommandSource>(
base::BindRepeating([](const std::u16string&, Browser* browser) {
auto outer = CreateNoOpCommandItem(u"outer", 100);
CommandItem::CompositeCommandProvider provider =
base::BindRepeating([](const std::u16string&) {
CommandSource::CommandResults results;
auto inner = CreateNoOpCommandItem(u"inner", 100);
inner->command = base::MakeExpectedRunClosure(FROM_HERE);
results.push_back(std::move(inner));
return results;
});
outer->command = std::make_pair(u"Do stuff", provider);
CommandSource::CommandResults result;
result.push_back(std::move(outer));
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(u"abracadabra", browser());
}
ASSERT_EQ(received_view_models_.size(), 1u);
// Select composite command.
{
ViewModelCallbackWaiter waiter(this);
controller->OnCommandSelected(0,
received_view_models_.back().result_set_id);
}
// Query again. Controller should pull results from the composite provider
// this time.
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(u"hocus pocus", browser());
}
ASSERT_EQ(received_view_models_.size(), 3u);
EXPECT_EQ(received_view_models_.back().items[0].title, u"inner");
controller->OnCommandSelected(0, received_view_models_.back().result_set_id);
// Inner command is an ExpectedRunClosure, so we will fail here if it wasn't
// called, without needing to assert anything.
}
TEST_F(CommanderControllerTest, OnCompositeCommandCancelledRemovesProvider) {
std::vector<std::unique_ptr<CommandSource>> sources;
TestCommandSource* source = AddSource(
&sources,
std::make_unique<TestCommandSource>(
base::BindRepeating([](const std::u16string&, Browser* browser) {
auto item = CreateNoOpCommandItem(u"first", 100);
CommandItem::CompositeCommandProvider noop =
base::BindRepeating([](const std::u16string&) {
return CommandSource::CommandResults();
});
item->command = std::make_pair(u"Do stuff", noop);
CommandSource::CommandResults result;
result.push_back(std::move(item));
return result;
})));
auto controller =
CommanderController::CreateWithSourcesForTesting(std::move(sources));
controller->SetUpdateCallback(base::BindRepeating(
&CommanderControllerTest::OnViewModelUpdated, base::Unretained(this)));
// Prime the sources so we can select an item.
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(u"abracadabra", browser());
}
EXPECT_EQ(source->invocations().size(), 1u);
// Selecting
{
ViewModelCallbackWaiter waiter(this);
controller->OnCommandSelected(0,
received_view_models_.back().result_set_id);
}
ASSERT_EQ(received_view_models_.size(), 2u);
EXPECT_EQ(received_view_models_.back().action,
CommanderViewModel::Action::kPrompt);
// This should go to the provider and not be seen by the source.
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(u"alakazam", browser());
}
EXPECT_EQ(source->invocations().size(), 1u);
controller->OnCompositeCommandCancelled();
// Composite command was cancelled, so the source should see this one.
{
ViewModelCallbackWaiter waiter(this);
controller->OnTextChanged(u"hocus pocus", browser());
}
EXPECT_EQ(source->invocations().size(), 2u);
EXPECT_EQ(source->invocations().back(), u"hocus pocus");
}
} // namespace commander