blob: e474cab842c893f820409c126949efda5850f125 [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 "peridot/bin/suggestion_engine/suggestion_engine_impl.h"
#include <fuchsia/modular/cpp/fidl.h>
#include <fuchsia/modular/internal/cpp/fidl.h>
#include <lib/fsl/vmo/strings.h>
#include "gtest/gtest.h"
#include "peridot/bin/sessionmgr/puppet_master/puppet_master_impl.h"
#include "peridot/bin/suggestion_engine/proposal_publisher_impl.h"
#include "peridot/lib/testing/test_story_command_executor.h"
#include "peridot/lib/testing/test_with_session_storage.h"
namespace modular {
namespace {
class TestNextListener : public fuchsia::modular::NextListener {
public:
void OnNextResults(
std::vector<fuchsia::modular::Suggestion> suggestions) override {
last_suggestions_ = std::move(suggestions);
}
void Reset() { last_suggestions_.clear(); }
void OnProcessingChange(bool processing) override{};
std::vector<fuchsia::modular::Suggestion>& last_suggestions() {
return last_suggestions_;
}
private:
std::vector<fuchsia::modular::Suggestion> last_suggestions_;
};
class TestInterruptionListener : public fuchsia::modular::InterruptionListener {
public:
void OnInterrupt(fuchsia::modular::Suggestion suggestion) override {
last_suggestion_ = std::move(suggestion);
}
fuchsia::modular::Suggestion& last_suggestion() { return last_suggestion_; }
private:
fuchsia::modular::Suggestion last_suggestion_;
};
class TestNavigationListener : public fuchsia::modular::NavigationListener {
public:
void OnNavigation(fuchsia::modular::NavigationAction navigation) override {
last_navigation_action_ = std::move(navigation);
}
fuchsia::modular::NavigationAction last_navigation_action() {
return last_navigation_action_;
}
private:
fuchsia::modular::NavigationAction last_navigation_action_;
};
class TestContextReaderImpl : public fuchsia::modular::ContextReader {
public:
TestContextReaderImpl(
fidl::InterfaceRequest<fuchsia::modular::ContextReader> request)
: binding_(this, std::move(request)) {}
private:
// |fuchsia::modular::ContextReader|
void Subscribe(fuchsia::modular::ContextQuery query,
fidl::InterfaceHandle<fuchsia::modular::ContextListener>
listener) override {}
// |fuchsia::modular::ContextReader|
void Get(fuchsia::modular::ContextQuery query,
fuchsia::modular::ContextReader::GetCallback callback) override {}
fidl::Binding<fuchsia::modular::ContextReader> binding_;
};
class SuggestionEngineTest : public testing::TestWithSessionStorage {
public:
SuggestionEngineTest()
: next_listener_binding_(&next_listener_),
interruption_listener_binding_(&interruption_listener_),
navigation_listener_binding_(&navigation_listener_) {}
void SetUp() override {
TestWithSessionStorage::SetUp();
// Get an unbound handles. We won't make use of these interfaces during the
// test except for our mock puppet master.
fuchsia::modular::ContextReaderPtr context_reader;
context_reader_impl_ =
std::make_unique<TestContextReaderImpl>(context_reader.NewRequest());
session_storage_ = MakeSessionStorage("page");
puppet_master_impl_ = std::make_unique<PuppetMasterImpl>(
session_storage_.get(), &test_executor_);
fuchsia::modular::PuppetMasterPtr puppet_master;
puppet_master_impl_->Connect(puppet_master.NewRequest());
suggestion_engine_impl_ = std::make_unique<SuggestionEngineImpl>(
std::move(context_reader), std::move(puppet_master));
suggestion_engine_impl_->Connect(engine_ptr_.NewRequest());
suggestion_engine_impl_->Connect(provider_ptr_.NewRequest());
suggestion_engine_impl_->Connect(debug_ptr_.NewRequest());
proposal_publisher_ = std::make_unique<ProposalPublisherImpl>(
suggestion_engine_impl_.get(), "Proposinator");
}
protected:
void StartListeningForNext(int max_suggestions) {
suggestion_engine_impl_->SubscribeToNext(
next_listener_binding_.NewBinding(), max_suggestions);
next_listener_.Reset();
}
void StartListeningForInterruptions() {
suggestion_engine_impl_->SubscribeToInterruptions(
interruption_listener_binding_.NewBinding());
}
void StartListeningForNavigation() {
suggestion_engine_impl_->SubscribeToNavigation(
navigation_listener_binding_.NewBinding());
}
fuchsia::modular::Proposal MakeProposal(const std::string& id,
const std::string& headline) {
fuchsia::modular::SuggestionDisplay display;
display.headline = headline;
fuchsia::modular::Proposal proposal;
proposal.id = id;
proposal.display = std::move(display);
return proposal;
}
fuchsia::modular::Proposal MakeInterruptionProposal(
const std::string id, const std::string& headline,
fuchsia::modular::AnnoyanceType annoyance =
fuchsia::modular::AnnoyanceType::INTERRUPT) {
auto proposal = MakeProposal(id, headline);
proposal.display.annoyance = annoyance;
return proposal;
}
fuchsia::modular::Proposal MakeRichProposal(const std::string id,
const std::string& headline) {
auto proposal = MakeProposal(id, headline);
proposal.wants_rich_suggestion = true;
return proposal;
}
void AddAddModuleAction(fuchsia::modular::Proposal* proposal,
const std::string& mod_name,
const std::string& mod_url,
const std::string& parent_mod = "",
fuchsia::modular::SurfaceArrangement arrangement =
fuchsia::modular::SurfaceArrangement::NONE) {
fuchsia::modular::Intent intent;
intent.handler = mod_url;
fuchsia::modular::AddMod add_mod;
add_mod.mod_name.push_back(mod_name);
add_mod.intent = std::move(intent);
if (!parent_mod.empty()) {
add_mod.surface_parent_mod_name.push_back(parent_mod);
}
add_mod.surface_relation.arrangement = arrangement;
fuchsia::modular::StoryCommand command;
command.set_add_mod(std::move(add_mod));
proposal->on_selected.push_back(std::move(command));
}
void AddFocusStoryAction(fuchsia::modular::Proposal* proposal) {
fuchsia::modular::SetFocusState focus_story;
focus_story.focused = true;
fuchsia::modular::StoryCommand command;
command.set_set_focus_state(std::move(focus_story));
proposal->on_selected.push_back(std::move(command));
}
void AddFocusModuleAction(fuchsia::modular::Proposal* proposal,
const std::string& mod_name) {
fuchsia::modular::FocusMod focus_mod;
focus_mod.mod_name.push_back(mod_name);
fuchsia::modular::StoryCommand command;
command.set_focus_mod(std::move(focus_mod));
proposal->on_selected.push_back(std::move(command));
}
void AddSetLinkValueAction(fuchsia::modular::Proposal* proposal,
const std::string& mod_name,
const std::string& link_name,
const std::string& link_value) {
fuchsia::modular::LinkPath link_path;
link_path.module_path.push_back(mod_name);
link_path.link_name = link_name;
fuchsia::modular::SetLinkValue set_link_value;
set_link_value.path = std::move(link_path);
fsl::SizedVmo vmo;
FXL_CHECK(fsl::VmoFromString(link_value, &vmo));
set_link_value.value =
std::make_unique<fuchsia::mem::Buffer>(std::move(vmo).ToTransport());
fuchsia::modular::StoryCommand command;
command.set_set_link_value(std::move(set_link_value));
proposal->on_selected.push_back(std::move(command));
}
std::unique_ptr<ProposalPublisherImpl> proposal_publisher_;
std::unique_ptr<SessionStorage> session_storage_;
std::unique_ptr<PuppetMasterImpl> puppet_master_impl_;
std::unique_ptr<SuggestionEngineImpl> suggestion_engine_impl_;
std::unique_ptr<TestContextReaderImpl> context_reader_impl_;
fuchsia::modular::SuggestionEnginePtr engine_ptr_;
fuchsia::modular::SuggestionProviderPtr provider_ptr_;
fuchsia::modular::SuggestionDebugPtr debug_ptr_;
testing::TestStoryCommandExecutor test_executor_;
TestNextListener next_listener_;
fidl::Binding<fuchsia::modular::NextListener> next_listener_binding_;
TestInterruptionListener interruption_listener_;
fidl::Binding<fuchsia::modular::InterruptionListener>
interruption_listener_binding_;
TestNavigationListener navigation_listener_;
fidl::Binding<fuchsia::modular::NavigationListener>
navigation_listener_binding_;
};
TEST_F(SuggestionEngineTest, AddNextProposal) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeProposal("1", "test_proposal");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntilIdle();
// We should see proposal in listener.
auto& results = next_listener_.last_suggestions();
ASSERT_EQ(1u, results.size());
EXPECT_EQ("test_proposal", results.at(0).display.headline);
}
TEST_F(SuggestionEngineTest, OnlyGetsMaxProposals) {
StartListeningForNext(2);
// Add three proposals.
proposal_publisher_->Propose(MakeProposal("1", "foo"));
proposal_publisher_->Propose(MakeProposal("2", "bar"));
proposal_publisher_->Propose(MakeProposal("3", "baz"));
RunLoopUntilIdle();
// We should see 2 proposals in listener.
auto& results = next_listener_.last_suggestions();
ASSERT_EQ(2u, results.size());
EXPECT_EQ("foo", results.at(0).display.headline);
EXPECT_EQ("bar", results.at(1).display.headline);
}
TEST_F(SuggestionEngineTest, AddNextProposalInterruption) {
StartListeningForNext(10);
StartListeningForInterruptions();
// Add interruptive proposal.
proposal_publisher_->Propose(MakeInterruptionProposal("1", "foo"));
RunLoopUntilIdle();
// Ensure notification.
auto& last_interruption = interruption_listener_.last_suggestion();
EXPECT_EQ("foo", last_interruption.display.headline);
// Suggestion shouldn't be in NEXT yet since it's interrupting.
auto& results = next_listener_.last_suggestions();
EXPECT_TRUE(results.empty());
}
TEST_F(SuggestionEngineTest, AddNextProposalRichNotAllowed) {
StartListeningForNext(10);
// Register publisher that can't submit rich proposals (see the url) and add
// proposal.
auto publisher = std::make_unique<ProposalPublisherImpl>(
suggestion_engine_impl_.get(), "foo");
publisher->Propose(MakeRichProposal("1", "foo"));
RunLoopUntilIdle();
// Suggestion shouldn't be rich: it has no preloaded story_id.
auto& results = next_listener_.last_suggestions();
ASSERT_EQ(1u, results.size());
EXPECT_EQ("foo", results.at(0).display.headline);
EXPECT_TRUE(results.at(0).preloaded_story_id->empty());
}
TEST_F(SuggestionEngineTest, AddNextProposalRich) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeRichProposal("1", "foo_rich");
AddAddModuleAction(&proposal, "mod_name", "mod_url", "parent_mod",
fuchsia::modular::SurfaceArrangement::ONTOP);
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntil([&] { return next_listener_.last_suggestions().size() == 1; });
// Suggestion should be rich: it has a preloaded story_id.
auto& results = next_listener_.last_suggestions();
EXPECT_EQ("foo_rich", results.at(0).display.headline);
EXPECT_FALSE(results.at(0).preloaded_story_id->empty());
auto story_name = results.at(0).preloaded_story_id;
// The executor should have been called with a command to add a mod and
// created a story.
EXPECT_EQ(1, test_executor_.execute_count());
EXPECT_FALSE(test_executor_.last_story_id()->empty());
auto& commands = test_executor_.last_commands();
ASSERT_EQ(1u, commands.size());
ASSERT_TRUE(commands.at(0).is_add_mod());
auto& command = commands.at(0).add_mod();
ASSERT_EQ(1u, command.mod_name.size());
EXPECT_EQ("mod_name", command.mod_name.at(0));
EXPECT_EQ("mod_url", command.intent.handler);
EXPECT_EQ(fuchsia::modular::SurfaceArrangement::ONTOP,
command.surface_relation.arrangement);
ASSERT_EQ(1u, command.surface_parent_mod_name->size());
EXPECT_EQ("parent_mod", command.surface_parent_mod_name->at(0));
// Ensure the story was created as kind-of-proto story.
bool done{};
session_storage_->GetStoryData(story_name)
->Then([&](fuchsia::modular::internal::StoryDataPtr story_data) {
ASSERT_NE(nullptr, story_data);
EXPECT_TRUE(story_data->story_options.kind_of_proto_story);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(SuggestionEngineTest, AddNextProposalRichReusesStory) {
StartListeningForNext(10);
auto story_name = "rich_story";
// Add proposal.
{
auto proposal = MakeRichProposal("1", "foo_rich");
proposal.story_name = story_name;
AddAddModuleAction(&proposal, "mod_name", "mod_url", "parent_mod",
fuchsia::modular::SurfaceArrangement::ONTOP);
proposal_publisher_->Propose(std::move(proposal));
}
RunLoopUntil([&] { return next_listener_.last_suggestions().size() == 1; });
// Up to here we expect the same as in the previous test (AddNextProposalRich)
// Submitting a new proposal with the same story_name should result on its
// story being directly updated and no notifications of new suggestions.
next_listener_.Reset();
test_executor_.Reset();
{
auto proposal = MakeRichProposal("1", "foo_rich");
proposal.story_name = story_name;
AddAddModuleAction(&proposal, "mod_name", "mod_url", "parent_mod",
fuchsia::modular::SurfaceArrangement::COPRESENT);
proposal_publisher_->Propose(std::move(proposal));
}
RunLoopUntil([&] { return test_executor_.execute_count() == 1; });
EXPECT_TRUE(next_listener_.last_suggestions().empty());
// The executor should have been called with a command to add a mod and
// created a story.
EXPECT_EQ(1, test_executor_.execute_count());
EXPECT_FALSE(test_executor_.last_story_id()->empty());
auto& commands = test_executor_.last_commands();
ASSERT_EQ(1u, commands.size());
ASSERT_TRUE(commands.at(0).is_add_mod());
auto& command = commands.at(0).add_mod();
ASSERT_EQ(1u, command.mod_name.size());
EXPECT_EQ("mod_name", command.mod_name.at(0));
EXPECT_EQ("mod_url", command.intent.handler);
EXPECT_EQ(fuchsia::modular::SurfaceArrangement::COPRESENT,
command.surface_relation.arrangement);
ASSERT_EQ(1u, command.surface_parent_mod_name->size());
EXPECT_EQ("parent_mod", command.surface_parent_mod_name->at(0));
// Ensure the story is there.
bool done{};
session_storage_->GetStoryData(story_name)
->Then([&](fuchsia::modular::internal::StoryDataPtr story_data) {
ASSERT_NE(nullptr, story_data);
EXPECT_TRUE(story_data->story_options.kind_of_proto_story);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(SuggestionEngineTest, AddNextProposalRichRespectsStoryName) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeRichProposal("1", "foo_rich");
proposal.story_name = "foo_story";
AddAddModuleAction(&proposal, "mod_name", "mod_url", "parent_mod",
fuchsia::modular::SurfaceArrangement::ONTOP);
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntil([&] { return next_listener_.last_suggestions().size() == 1; });
// Suggestion should be rich: it has a preloaded story_id.
auto& results = next_listener_.last_suggestions();
EXPECT_EQ("foo_story", results.at(0).preloaded_story_id);
// The executor should have been called with a command to add a mod and
// created a story.
EXPECT_EQ(1, test_executor_.execute_count());
EXPECT_EQ("foo_story", test_executor_.last_story_id());
// Ensure the story was created as kind-of-proto story.
bool done{};
session_storage_->GetStoryData("foo_story")
->Then([&](fuchsia::modular::internal::StoryDataPtr story_data) {
ASSERT_NE(nullptr, story_data);
EXPECT_TRUE(story_data->story_options.kind_of_proto_story);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(SuggestionEngineTest, RemoveNextProposal) {
StartListeningForNext(10);
// Add proposal
proposal_publisher_->Propose(MakeProposal("1", "foo"));
// Remove proposal
proposal_publisher_->Remove("1");
RunLoopUntilIdle();
auto& results = next_listener_.last_suggestions();
EXPECT_TRUE(results.empty());
}
TEST_F(SuggestionEngineTest, RemoveNextProposalRich) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeRichProposal("1", "foo_rich");
proposal.story_name = "foo_story";
proposal_publisher_->Propose(std::move(proposal));
// TODO(miguelfrde): add an operation queue in the suggestion engine and
// remove this wait.
RunLoopUntil([&] { return next_listener_.last_suggestions().size() == 1; });
// Remove proposal.
proposal_publisher_->Remove("1");
RunLoopUntil([&] { return next_listener_.last_suggestions().empty(); });
// The story that at some point was created when adding the rich suggestion
// (not tested since other tests already cover it) should have been deleted.
bool done{};
session_storage_->GetStoryData("foo_story")
->Then([&](fuchsia::modular::internal::StoryDataPtr story_data) {
EXPECT_EQ(nullptr, story_data);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(SuggestionEngineTest, NotifyInteractionSelected) {
StartListeningForNext(10);
// Add proposal. One action of each action we support that translates to
// StoryCommand is added. This set of actions doesn't really make sense in an
// actual use case.
auto proposal = MakeProposal("1", "foo");
AddAddModuleAction(&proposal, "mod_name", "mod_url");
AddFocusStoryAction(&proposal);
AddFocusModuleAction(&proposal, "mod_name");
AddSetLinkValueAction(&proposal, "mod_name", "foo_link_name", "foo_value");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntilIdle();
// Get id of the resulting suggestion.
auto& results = next_listener_.last_suggestions();
ASSERT_EQ(1u, results.size());
auto suggestion_id = results.at(0).uuid;
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::SELECTED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntil([&] { return test_executor_.execute_count() == 1; });
// The executor should have been called with the right commands.
auto story_id = test_executor_.last_story_id();
auto& commands = test_executor_.last_commands();
ASSERT_EQ(4u, commands.size());
EXPECT_TRUE(commands.at(0).is_add_mod());
EXPECT_TRUE(commands.at(1).is_set_focus_state());
EXPECT_TRUE(commands.at(2).is_focus_mod());
EXPECT_TRUE(commands.at(3).is_set_link_value());
auto& add_mod = commands.at(0).add_mod();
ASSERT_EQ(1u, add_mod.mod_name.size());
EXPECT_EQ("mod_name", add_mod.mod_name.at(0));
EXPECT_EQ("mod_url", add_mod.intent.handler);
auto& set_focus_state = commands.at(1).set_focus_state();
EXPECT_TRUE(set_focus_state.focused);
auto& focus_mod = commands.at(2).focus_mod();
ASSERT_EQ(1u, focus_mod.mod_name.size());
EXPECT_EQ("mod_name", focus_mod.mod_name.at(0));
auto& set_link_value = commands.at(3).set_link_value();
ASSERT_EQ(1u, set_link_value.path.module_path.size());
EXPECT_EQ("mod_name", set_link_value.path.module_path.at(0));
EXPECT_EQ("foo_link_name", set_link_value.path.link_name);
std::string link_value;
FXL_CHECK(fsl::StringFromVmo(*set_link_value.value, &link_value));
EXPECT_EQ("foo_value", link_value);
// Ensure a regular story was created when we executed the proposal.
bool done{};
session_storage_->GetStoryData(story_id)->Then(
[&](fuchsia::modular::internal::StoryDataPtr story_data) {
EXPECT_NE(nullptr, story_data);
EXPECT_FALSE(story_data->story_options.kind_of_proto_story);
done = true;
});
RunLoopUntil([&] { return done; });
// We should have been notified with no suggestions after selecting this
// suggestion.
auto& listener_results = next_listener_.last_suggestions();
EXPECT_TRUE(listener_results.empty());
}
TEST_F(SuggestionEngineTest, NotifyInteractionSelectedWithStoryName) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeProposal("1", "foo");
proposal.story_name = "foo_story";
AddFocusModuleAction(&proposal, "mod_name");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntilIdle();
// Get id of the resulting suggestion.
auto& results = next_listener_.last_suggestions();
ASSERT_EQ(1u, results.size());
auto suggestion_id = results.at(0).uuid;
// Select suggestion.
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::SELECTED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntil([&] { return test_executor_.execute_count() == 1; });
// The executor should have been called with the command associated to the
// action added above.
EXPECT_EQ("foo_story", test_executor_.last_story_id());
auto& commands = test_executor_.last_commands();
ASSERT_EQ(1u, commands.size());
EXPECT_TRUE(commands.at(0).is_focus_mod());
auto& focus_mod = commands.at(0).focus_mod();
ASSERT_EQ(1u, focus_mod.mod_name.size());
EXPECT_EQ("mod_name", focus_mod.mod_name.at(0));
// Ensure a regular story was created when we executed the proposal.
bool done{};
session_storage_->GetStoryData("foo_story")
->Then([&](fuchsia::modular::internal::StoryDataPtr story_data) {
EXPECT_NE(nullptr, story_data);
EXPECT_FALSE(story_data->story_options.kind_of_proto_story);
done = true;
});
RunLoopUntil([&] { return done; });
// We should have been notified with no suggestions after selecting this
// suggestion.
auto& listener_results = next_listener_.last_suggestions();
EXPECT_TRUE(listener_results.empty());
}
TEST_F(SuggestionEngineTest, NotifyInteractionDismissed) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeProposal("1", "foo");
AddFocusModuleAction(&proposal, "mod_name");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntilIdle();
// Get id of the resulting suggestion.
auto& results = next_listener_.last_suggestions();
ASSERT_EQ(1u, results.size());
auto suggestion_id = results.at(0).uuid;
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::DISMISSED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntilIdle();
// The executor shouldn't have been called.
EXPECT_EQ(0, test_executor_.execute_count());
// We should have been notified with no suggestions after dismissing this
// suggestion.
auto& listener_results = next_listener_.last_suggestions();
EXPECT_TRUE(listener_results.empty());
}
TEST_F(SuggestionEngineTest, NotifyInteractionDismissedWithStoryName) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeProposal("1", "foo");
proposal.story_name = "foo_story";
AddFocusModuleAction(&proposal, "mod_name");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntilIdle();
// Get id of the resulting suggestion.
auto& results = next_listener_.last_suggestions();
ASSERT_EQ(1u, results.size());
auto suggestion_id = results.at(0).uuid;
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::DISMISSED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntilIdle();
// The executor shouldn't have been called.
EXPECT_EQ(0, test_executor_.execute_count());
// We should have been notified with no suggestions after dismissing this
// suggestion.
auto& listener_results = next_listener_.last_suggestions();
EXPECT_TRUE(listener_results.empty());
// Ensure no story was created when we executed the proposal.
bool done{};
session_storage_->GetStoryData("foo_story")
->Then([&](fuchsia::modular::internal::StoryDataPtr story_data) {
EXPECT_EQ(nullptr, story_data);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(SuggestionEngineTest, NotifyInteractionSelectedRich) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeRichProposal("1", "foo_rich");
AddFocusModuleAction(&proposal, "mod_name");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntil([&] { return next_listener_.last_suggestions().size() == 1; });
// Get id of the resulting suggestion.
auto& results = next_listener_.last_suggestions();
auto suggestion_id = results.at(0).uuid;
EXPECT_FALSE(results.at(0).preloaded_story_id->empty());
auto story_name = results.at(0).preloaded_story_id;
test_executor_.Reset();
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::SELECTED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntilIdle();
// The executor should have been called for a second time with a command to
// promote the story that the adding of the the proposal created.
EXPECT_EQ(test_executor_.execute_count(), 0);
// Ensure the story that was created when we adedd the rich proposal still
// exists.
bool done{};
session_storage_->GetStoryData(story_name)
->Then([&](fuchsia::modular::internal::StoryDataPtr story_data) {
EXPECT_NE(nullptr, story_data);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(SuggestionEngineTest, NotifyInteractionDismissedRich) {
StartListeningForNext(10);
// Add proposal.
auto proposal = MakeRichProposal("1", "foo_rich");
AddFocusModuleAction(&proposal, "mod_name");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntil([&] { return next_listener_.last_suggestions().size() == 1; });
// Get id and story of the resulting suggestion.
auto& results = next_listener_.last_suggestions();
EXPECT_EQ(1, test_executor_.execute_count());
auto suggestion_id = results.at(0).uuid;
EXPECT_FALSE(results.at(0).preloaded_story_id->empty());
auto story_name = results.at(0).preloaded_story_id;
test_executor_.Reset();
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::DISMISSED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntil([&] { return next_listener_.last_suggestions().empty(); });
// The executor shouldn't have been called again.
EXPECT_EQ(0, test_executor_.execute_count());
// Ensure the story that was created when we added the rich proposal is gone.
bool done{};
session_storage_->GetStoryData(story_name)
->Then([&](fuchsia::modular::internal::StoryDataPtr story_data) {
EXPECT_EQ(nullptr, story_data);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(SuggestionEngineTest, NotifyInteractionSnoozedInterruption) {
StartListeningForInterruptions();
StartListeningForNext(10);
// Add interruptive proposal.
proposal_publisher_->Propose(MakeInterruptionProposal("1", "foo"));
RunLoopUntilIdle();
// Get id of the resulting suggestion.
auto& suggestion = interruption_listener_.last_suggestion();
EXPECT_FALSE(suggestion.uuid.empty());
auto suggestion_id = suggestion.uuid;
EXPECT_TRUE(next_listener_.last_suggestions().empty());
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::SNOOZED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntil([&] { return next_listener_.last_suggestions().size() == 1; });
// The suggestion should still be there after being notified.
auto& listener_results = next_listener_.last_suggestions();
EXPECT_EQ(suggestion_id, listener_results.at(0).uuid);
}
TEST_F(SuggestionEngineTest, NotifyInteractionExpiredInterruption) {
StartListeningForInterruptions();
StartListeningForNext(10);
// Add interruptive proposal.
proposal_publisher_->Propose(MakeInterruptionProposal("1", "foo"));
RunLoopUntilIdle();
// Get id of the resulting suggestion.
auto& suggestion = interruption_listener_.last_suggestion();
EXPECT_FALSE(suggestion.uuid.empty());
auto suggestion_id = suggestion.uuid;
EXPECT_TRUE(next_listener_.last_suggestions().empty());
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::EXPIRED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntilIdle();
// The suggestion should still be there after being notified.
auto& listener_results = next_listener_.last_suggestions();
EXPECT_EQ(1u, listener_results.size());
EXPECT_EQ(suggestion_id, listener_results.at(0).uuid);
}
TEST_F(SuggestionEngineTest, NotifyInteractionSelectedInterruption) {
StartListeningForInterruptions();
StartListeningForNext(10);
// Add interruptive proposal.
auto proposal = MakeInterruptionProposal("1", "foo");
AddFocusModuleAction(&proposal, "mod_name");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntilIdle();
auto& suggestion = interruption_listener_.last_suggestion();
EXPECT_FALSE(suggestion.uuid.empty());
auto suggestion_id = suggestion.uuid;
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::SELECTED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntil([&] { return test_executor_.execute_count() == 1; });
// The executor should have been called with a command to add a mod and
// created a story.
auto story_id = test_executor_.last_story_id();
auto& commands = test_executor_.last_commands();
ASSERT_EQ(1u, commands.size());
auto& focus_mod = commands.at(0).focus_mod();
ASSERT_EQ(1u, focus_mod.mod_name.size());
EXPECT_EQ("mod_name", focus_mod.mod_name.at(0));
// Ensure a regular story was created when we executed the proposal.
bool done{};
session_storage_->GetStoryData(story_id)->Then(
[&](fuchsia::modular::internal::StoryDataPtr story_data) {
EXPECT_NE(nullptr, story_data);
EXPECT_FALSE(story_data->story_options.kind_of_proto_story);
done = true;
});
RunLoopUntil([&] { return done; });
// The suggestion shouldn't be there anymore.
EXPECT_TRUE(next_listener_.last_suggestions().empty());
}
TEST_F(SuggestionEngineTest, NotifyInteractionDismissedInterruption) {
StartListeningForInterruptions();
StartListeningForNext(10);
// Add interruptive proposal.
auto proposal = MakeInterruptionProposal("1", "foo");
AddFocusModuleAction(&proposal, "mod_name");
proposal_publisher_->Propose(std::move(proposal));
RunLoopUntilIdle();
auto& suggestion = interruption_listener_.last_suggestion();
EXPECT_FALSE(suggestion.uuid.empty());
auto suggestion_id = suggestion.uuid;
fuchsia::modular::Interaction interaction;
interaction.type = fuchsia::modular::InteractionType::DISMISSED;
suggestion_engine_impl_->NotifyInteraction(suggestion_id,
std::move(interaction));
RunLoopUntilIdle();
// The executor shouldn't have been called.
EXPECT_EQ(0, test_executor_.execute_count());
// The suggestion shouldn't be there anymore.
EXPECT_TRUE(next_listener_.last_suggestions().empty());
}
TEST_F(SuggestionEngineTest, ProposeNavigation) {
StartListeningForNavigation();
proposal_publisher_->ProposeNavigation(
fuchsia::modular::NavigationAction::HOME);
RunLoopUntilIdle();
EXPECT_EQ(fuchsia::modular::NavigationAction::HOME,
navigation_listener_.last_navigation_action());
}
} // namespace
} // namespace modular