| // 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_transitional = 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_transitional = 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_FALSE(command.mod_name_transitional.is_null()); |
| EXPECT_EQ("mod_name", command.mod_name_transitional.get()); |
| 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_FALSE(command.mod_name_transitional.is_null()); |
| EXPECT_EQ("mod_name", command.mod_name_transitional.get()); |
| 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_FALSE(add_mod.mod_name_transitional.is_null()); |
| EXPECT_EQ("mod_name", add_mod.mod_name_transitional.get()); |
| 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_FALSE(focus_mod.mod_name_transitional.is_null()); |
| EXPECT_EQ("mod_name", focus_mod.mod_name_transitional.get()); |
| |
| 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_FALSE(focus_mod.mod_name_transitional.is_null()); |
| EXPECT_EQ("mod_name", focus_mod.mod_name_transitional.get()); |
| |
| // 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_FALSE(focus_mod.mod_name_transitional.is_null()); |
| EXPECT_EQ("mod_name", focus_mod.mod_name_transitional.get()); |
| |
| // 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 |