[modular][testing] Rewrite the session_shell integration test with modular test harness.
See peridot/tests/session_shell/session_shell_test_session_shell.cc for
the test this is trying to replace.
A few things to note here:
- Factoring some of the boilerplate used to intercept the session shell
and initialize the mock into a helper function is a possibly
controversial choice. I think it's worthwhile here, since it's not
really necessary for understanding the test.
- The mock session shell used here is not in its final form, as noted in
the TODO. There is another issue (MF-389) that will refactor it into a
utility class.
- The final test, which tests whether DetachView() is called during
logout, is not implemented yet. It will probably add some significant
complexity / length since it will have to intercept and mock base shell, so I
decided to do that in a future change (this change is already too big).
MF-399
Change-Id: I389d78b6db3c345e5f085826492dc390ce9f9fad
diff --git a/src/modular/tests/BUILD.gn b/src/modular/tests/BUILD.gn
index 779d9c1..742b6b7 100644
--- a/src/modular/tests/BUILD.gn
+++ b/src/modular/tests/BUILD.gn
@@ -33,16 +33,41 @@
]
}
+executable("session_shell_test") {
+ testonly = true
+
+ sources = [
+ "session_shell_test.cc",
+ ]
+
+ deps = [
+ "//garnet/public/lib/fsl",
+ "//peridot/lib/testing:session_shell_base",
+ "//peridot/public/lib/modular_test_harness/cpp:test_harness_fixture",
+ "//sdk/fidl/fuchsia.modular.testing",
+ "//sdk/fidl/fuchsia.sys",
+ "//sdk/lib/sys/cpp",
+ "//sdk/lib/sys/cpp/testing:integration",
+ "//third_party/googletest:gmock",
+ "//third_party/googletest:gtest_main",
+ ]
+}
+
test_package("modular_integration_tests") {
tests = [
{
name = "last_focus_time_test"
environments = basic_envs
},
+ {
+ name = "session_shell_test"
+ environments = basic_envs
+ },
]
deps = [
":last_focus_time_test",
+ ":session_shell_test",
"//garnet/public/lib/callback",
"//peridot/public/lib/app_driver/cpp",
"//sdk/fidl/fuchsia.modular",
diff --git a/src/modular/tests/meta/session_shell_test.cmx b/src/modular/tests/meta/session_shell_test.cmx
new file mode 100644
index 0000000..01a8282
--- /dev/null
+++ b/src/modular/tests/meta/session_shell_test.cmx
@@ -0,0 +1,11 @@
+{
+ "program": {
+ "binary": "test/session_shell_test"
+ },
+ "sandbox": {
+ "services": [
+ "fuchsia.sys.Environment",
+ "fuchsia.sys.Launcher"
+ ]
+ }
+}
diff --git a/src/modular/tests/session_shell_test.cc b/src/modular/tests/session_shell_test.cc
new file mode 100644
index 0000000..efcee58
--- /dev/null
+++ b/src/modular/tests/session_shell_test.cc
@@ -0,0 +1,521 @@
+// Copyright 2016 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 <fuchsia/modular/testing/cpp/fidl.h>
+#include <lib/fsl/vmo/strings.h>
+#include <lib/modular_test_harness/cpp/fake_component.h>
+#include <lib/modular_test_harness/cpp/test_harness_fixture.h>
+#include <peridot/lib/modular_config/modular_config.h>
+#include <peridot/lib/modular_config/modular_config_constants.h>
+#include <peridot/lib/testing/session_shell_base.h>
+#include <sdk/lib/sys/cpp/component_context.h>
+#include <sdk/lib/sys/cpp/service_directory.h>
+#include <sdk/lib/sys/cpp/testing/test_with_environment.h>
+
+#include "gmock/gmock.h"
+
+using fuchsia::modular::AddMod;
+using fuchsia::modular::StoryCommand;
+using fuchsia::modular::StoryInfo;
+using fuchsia::modular::StoryState;
+using fuchsia::modular::StoryVisibilityState;
+using fuchsia::modular::ViewIdentifier;
+using testing::IsNull;
+using testing::Not;
+
+constexpr char kFakeModuleUrl[] =
+ "fuchsia-pkg://example.com/FAKE_MODULE_PKG/fake_module.cmx";
+
+namespace {
+
+// Session shell mock that provides access to the StoryProvider and the
+// SessionShellContext. Also acts as a StoryProviderWatcher, and will run a
+// lambda function that can be provided by a test when it sees a change in story
+// state.
+//
+// TODO(MF-386): Factor this out into a generally usable mock session shell.
+class MockSessionShell : public modular::testing::FakeComponent,
+ fuchsia::modular::StoryProviderWatcher {
+ public:
+ MockSessionShell() : story_provider_watcher_(this) {}
+
+ fuchsia::modular::StoryProvider* GetStoryProvider() {
+ return story_provider_.get();
+ }
+
+ fuchsia::modular::SessionShellContext* GetSessionShellContext() {
+ return session_shell_context_.get();
+ }
+
+ modular::testing::SessionShellImpl* GetSessionShellImpl() {
+ return &session_shell_impl_;
+ }
+
+ using OnChangeFunction =
+ fit::function<void(StoryInfo, StoryState, StoryVisibilityState)>;
+
+ void SetOnChange(OnChangeFunction on_change) {
+ on_change_ = std::move(on_change);
+ }
+
+ private:
+ // |modular::testing::FakeComponent|
+ void OnCreate(fuchsia::sys::StartupInfo startup_info) override {
+ component_context()->svc()->Connect(session_shell_context_.NewRequest());
+ session_shell_context_->GetStoryProvider(story_provider_.NewRequest());
+
+ component_context()->outgoing()->AddPublicService(
+ session_shell_impl_.GetHandler());
+
+ story_provider_->GetStories(
+ story_provider_watcher_.NewBinding(),
+ [](std::vector<fuchsia::modular::StoryInfo> stories) {});
+ }
+
+ // |fuchsia::modular::StoryProviderWatcher|
+ void OnChange(StoryInfo story_info, StoryState story_state,
+ StoryVisibilityState story_visibility_state) override {
+ on_change_(std::move(story_info), story_state, story_visibility_state);
+ }
+
+ // |fuchsia::modular::StoryProviderWatcher|
+ void OnDelete(std::string story_id) override {}
+
+ fidl::Binding<StoryProviderWatcher> story_provider_watcher_;
+ // Optional user-provided lambda that will run with each OnChange(). Defaults
+ // to doing nothing.
+ OnChangeFunction on_change_ =
+ [](StoryInfo story_info, StoryState story_state,
+ StoryVisibilityState story_visibility_state) {};
+
+ modular::testing::SessionShellImpl session_shell_impl_;
+ fuchsia::modular::SessionShellContextPtr session_shell_context_;
+ fuchsia::modular::StoryProviderPtr story_provider_;
+};
+
+class SessionShellTest : public modular::testing::TestHarnessFixture {
+ protected:
+ // Shared boilerplate for configuring the test harness to intercept the
+ // session shell, setting up the session shell mock object, running the test
+ // harness, and waiting for the session shell to be successfully intercepted.
+ // Note that this method blocks the thread until the session shell has started
+ // up.
+ //
+ // Not done in SetUp() or the constructor to let the test reader know that
+ // this is happening. Also, certain tests may want to change this flow.
+ void RunHarnessAndInterceptSessionShell() {
+ modular::testing::TestHarnessBuilder builder;
+ builder.InterceptSessionShell(
+ mock_session_shell_.GetOnCreateHandler(),
+ {.sandbox_services = {"fuchsia.modular.SessionShellContext",
+ "fuchsia.modular.PuppetMaster"}});
+
+ test_harness().events().OnNewComponent =
+ builder.BuildOnNewComponentHandler();
+ test_harness()->Run(builder.BuildSpec());
+
+ // Wait for our session shell to start.
+ RunLoopUntil([&] { return mock_session_shell_.is_running(); });
+ }
+
+ MockSessionShell mock_session_shell_;
+};
+
+TEST_F(SessionShellTest, GetPackageName) {
+ fuchsia::modular::testing::TestHarnessSpec spec;
+ test_harness()->Run(std::move(spec));
+
+ fuchsia::modular::ComponentContextPtr component_context;
+ fuchsia::modular::testing::TestHarnessService svc;
+ svc.set_component_context(component_context.NewRequest());
+ test_harness()->GetService(std::move(svc));
+
+ bool got_name = false;
+ component_context->GetPackageName([&got_name](fidl::StringPtr name) {
+ EXPECT_THAT(name, Not(IsNull()));
+ got_name = true;
+ });
+
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return got_name; }, zx::sec(10)));
+}
+
+TEST_F(SessionShellTest, GetStoryInfoNonexistentStory) {
+ RunHarnessAndInterceptSessionShell();
+
+ fuchsia::modular::StoryProvider* story_provider =
+ mock_session_shell_.GetStoryProvider();
+ ASSERT_TRUE(story_provider != nullptr);
+
+ bool tried_get_story_info = false;
+ story_provider->GetStoryInfo(
+ "X", [&tried_get_story_info](fuchsia::modular::StoryInfoPtr story_info) {
+ EXPECT_THAT(story_info, IsNull());
+ tried_get_story_info = true;
+ });
+
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return tried_get_story_info; },
+ zx::sec(10)));
+}
+
+TEST_F(SessionShellTest, GetLink) {
+ RunHarnessAndInterceptSessionShell();
+
+ fuchsia::modular::SessionShellContext* session_shell_context;
+ session_shell_context = mock_session_shell_.GetSessionShellContext();
+ ASSERT_TRUE(session_shell_context != nullptr);
+
+ fuchsia::modular::LinkPtr session_shell_link;
+ session_shell_context->GetLink(session_shell_link.NewRequest());
+ bool called_get_link = false;
+ session_shell_link->Get(
+ nullptr, [&called_get_link](std::unique_ptr<fuchsia::mem::Buffer> value) {
+ called_get_link = true;
+ });
+
+ ASSERT_TRUE(
+ RunLoopWithTimeoutOrUntil([&] { return called_get_link; }, zx::sec(10)));
+}
+
+TEST_F(SessionShellTest, GetStoriesEmpty) {
+ RunHarnessAndInterceptSessionShell();
+
+ fuchsia::modular::StoryProvider* story_provider =
+ mock_session_shell_.GetStoryProvider();
+ ASSERT_TRUE(story_provider != nullptr);
+
+ bool called_get_stories = false;
+ story_provider->GetStories(
+ nullptr,
+ [&called_get_stories](std::vector<fuchsia::modular::StoryInfo> stories) {
+ EXPECT_THAT(stories, testing::IsEmpty());
+ called_get_stories = true;
+ });
+
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return called_get_stories; },
+ zx::sec(10)));
+}
+
+TEST_F(SessionShellTest, StartAndStopStoryWithExtraInfoMod) {
+ RunHarnessAndInterceptSessionShell();
+
+ // Create a new story using PuppetMaster and launch a new story shell,
+ // including a mod with extra info.
+ fuchsia::modular::PuppetMasterPtr puppet_master;
+ fuchsia::modular::StoryPuppetMasterPtr story_master;
+
+ fuchsia::modular::testing::TestHarnessService svc;
+ svc.set_puppet_master(puppet_master.NewRequest());
+ test_harness()->GetService(std::move(svc));
+
+ fuchsia::modular::StoryProvider* story_provider =
+ mock_session_shell_.GetStoryProvider();
+ ASSERT_TRUE(story_provider != nullptr);
+ const char kStoryId[] = "my_story";
+
+ // Have the mock session_shell record the sequence of story states it sees,
+ // and confirm that it only sees the correct story id.
+ std::vector<StoryState> sequence_of_story_states;
+ mock_session_shell_.SetOnChange(
+ [&sequence_of_story_states, kStoryId](StoryInfo story_info,
+ StoryState story_state,
+ StoryVisibilityState _) {
+ EXPECT_EQ(story_info.id, kStoryId);
+ sequence_of_story_states.push_back(story_state);
+ });
+ puppet_master->ControlStory(kStoryId, story_master.NewRequest());
+
+ AddMod add_mod;
+ add_mod.mod_name_transitional = "mod1";
+ add_mod.intent.handler = kFakeModuleUrl;
+ fuchsia::modular::IntentParameter param;
+ param.name = "root";
+ fsl::SizedVmo vmo;
+ const std::string initial_json = R"({"created-with-info": true})";
+ ASSERT_TRUE(fsl::VmoFromString(initial_json, &vmo));
+ param.data.set_json(std::move(vmo).ToTransport());
+ add_mod.intent.parameters.push_back(std::move(param));
+
+ StoryCommand command;
+ command.set_add_mod(std::move(add_mod));
+
+ std::vector<StoryCommand> commands;
+ commands.push_back(std::move(command));
+
+ story_master->Enqueue(std::move(commands));
+ bool execute_called = false;
+ story_master->Execute(
+ [&execute_called](fuchsia::modular::ExecuteResult result) {
+ execute_called = true;
+ });
+ ASSERT_TRUE(
+ RunLoopWithTimeoutOrUntil([&] { return execute_called; }, zx::sec(10)));
+
+ // Stop the story. Check that the story went through the correct sequence
+ // of states (see StoryState FIDL file for valid state transitions). Since we
+ // started it, ran it, and stopped it, the sequence is STOPPED -> RUNNING ->
+ // STOPPING -> STOPPED.
+ fuchsia::modular::StoryControllerPtr story_controller;
+ story_provider->GetController(kStoryId, story_controller.NewRequest());
+ bool stop_called = false;
+ story_controller->Stop([&stop_called] { stop_called = true; });
+ ASSERT_TRUE(
+ RunLoopWithTimeoutOrUntil([&] { return stop_called; }, zx::sec(10)));
+ // Run the loop until there are the expected number of state changes;
+ // having called Stop() is not enough to guarantee seeing all updates.
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
+ [&] { return sequence_of_story_states.size() == 4; }, zx::sec(10)));
+ EXPECT_THAT(sequence_of_story_states,
+ testing::ElementsAre(StoryState::STOPPED, StoryState::RUNNING,
+ StoryState::STOPPING, StoryState::STOPPED));
+}
+
+TEST_F(SessionShellTest, StoryInfoBeforeAndAfterDelete) {
+ RunHarnessAndInterceptSessionShell();
+
+ // Create a new story using PuppetMaster and launch a new story shell.
+ fuchsia::modular::PuppetMasterPtr puppet_master;
+ fuchsia::modular::StoryPuppetMasterPtr story_master;
+
+ fuchsia::modular::testing::TestHarnessService svc;
+ svc.set_puppet_master(puppet_master.NewRequest());
+ test_harness()->GetService(std::move(svc));
+
+ fuchsia::modular::StoryProvider* story_provider =
+ mock_session_shell_.GetStoryProvider();
+ ASSERT_TRUE(story_provider != nullptr);
+ const char kStoryId[] = "my_story";
+ puppet_master->ControlStory(kStoryId, story_master.NewRequest());
+
+ AddMod add_mod;
+ add_mod.mod_name_transitional = "mod1";
+ add_mod.intent.handler = kFakeModuleUrl;
+
+ StoryCommand command;
+ command.set_add_mod(std::move(add_mod));
+
+ std::vector<StoryCommand> commands;
+ commands.push_back(std::move(command));
+
+ story_master->Enqueue(std::move(commands));
+
+ bool execute_and_get_story_info_called = false;
+ story_master->Execute(
+ [&execute_and_get_story_info_called, kStoryId,
+ story_provider](fuchsia::modular::ExecuteResult result) {
+ // Verify that the newly created story returns something for
+ // GetStoryInfo().
+ story_provider->GetStoryInfo(
+ kStoryId, [&execute_and_get_story_info_called,
+ kStoryId](fuchsia::modular::StoryInfoPtr story_info) {
+ ASSERT_THAT(story_info, Not(IsNull()));
+ EXPECT_EQ(story_info->id, kStoryId);
+ execute_and_get_story_info_called = true;
+ });
+ });
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
+ [&] { return execute_and_get_story_info_called; }, zx::sec(10)));
+
+ // Delete the story and confirm that the story info is null now.
+ bool delete_called = false;
+ puppet_master->DeleteStory(
+ kStoryId, [&delete_called, kStoryId, story_provider] {
+ story_provider->GetStoryInfo(
+ kStoryId, [](fuchsia::modular::StoryInfoPtr story_info) {
+ EXPECT_THAT(story_info, IsNull());
+ });
+ delete_called = true;
+ });
+ ASSERT_TRUE(
+ RunLoopWithTimeoutOrUntil([&] { return delete_called; }, zx::sec(10)));
+}
+
+TEST_F(SessionShellTest, KindOfProtoStoryNotInStoryList) {
+ RunHarnessAndInterceptSessionShell();
+
+ // Create a new story using PuppetMaster and launch a new story shell,
+ // adding the kind of proto option.
+ fuchsia::modular::PuppetMasterPtr puppet_master;
+ fuchsia::modular::StoryPuppetMasterPtr story_master;
+
+ fuchsia::modular::testing::TestHarnessService svc;
+ svc.set_puppet_master(puppet_master.NewRequest());
+ test_harness()->GetService(std::move(svc));
+
+ fuchsia::modular::StoryProvider* story_provider =
+ mock_session_shell_.GetStoryProvider();
+ ASSERT_TRUE(story_provider != nullptr);
+
+ const char kStoryId[] = "my_story";
+ puppet_master->ControlStory(kStoryId, story_master.NewRequest());
+
+ fuchsia::modular::StoryOptions story_options;
+ story_options.kind_of_proto_story = true;
+ story_master->SetCreateOptions(std::move(story_options));
+
+ bool called_get_stories = false;
+ story_master->Execute([&called_get_stories, story_provider](
+ fuchsia::modular::ExecuteResult result) {
+ // Confirm that even after the story is created, GetStories() returns
+ // empty.
+ story_provider->GetStories(
+ nullptr, [&called_get_stories](
+ std::vector<fuchsia::modular::StoryInfo> stories) {
+ EXPECT_THAT(stories, testing::IsEmpty());
+ called_get_stories = true;
+ });
+ });
+
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return called_get_stories; },
+ zx::sec(10)));
+}
+
+TEST_F(SessionShellTest, AttachesAndDetachesView) {
+ RunHarnessAndInterceptSessionShell();
+
+ // Create a new story using PuppetMaster and start a new story shell.
+ // Confirm that AttachView() is called.
+ fuchsia::modular::PuppetMasterPtr puppet_master;
+ fuchsia::modular::StoryPuppetMasterPtr story_master;
+
+ fuchsia::modular::testing::TestHarnessService svc;
+ svc.set_puppet_master(puppet_master.NewRequest());
+ test_harness()->GetService(std::move(svc));
+
+ fuchsia::modular::StoryProvider* story_provider =
+ mock_session_shell_.GetStoryProvider();
+ ASSERT_TRUE(story_provider != nullptr);
+
+ const char kStoryId[] = "my_story";
+ // Have the mock session_shell record the sequence of story states it sees,
+ // and confirm that it only sees the correct story id.
+ std::vector<StoryState> sequence_of_story_states;
+ mock_session_shell_.SetOnChange(
+ [&sequence_of_story_states, kStoryId](StoryInfo story_info,
+ StoryState story_state,
+ StoryVisibilityState _) {
+ EXPECT_EQ(story_info.id, kStoryId);
+ sequence_of_story_states.push_back(story_state);
+ });
+ puppet_master->ControlStory(kStoryId, story_master.NewRequest());
+
+ AddMod add_mod;
+ add_mod.mod_name_transitional = "mod1";
+ add_mod.intent.handler = kFakeModuleUrl;
+
+ StoryCommand command;
+ command.set_add_mod(std::move(add_mod));
+
+ std::vector<StoryCommand> commands;
+ commands.push_back(std::move(command));
+
+ story_master->Enqueue(std::move(commands));
+ story_master->Execute([](fuchsia::modular::ExecuteResult result) {});
+
+ bool called_attach_view = false;
+ mock_session_shell_.GetSessionShellImpl()->set_on_attach_view(
+ [&called_attach_view](ViewIdentifier) { called_attach_view = true; });
+
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return called_attach_view; },
+ zx::sec(10)));
+
+ // Stop the story. Confirm that:
+ // a. DetachView() was called.
+ // b. The story went through the correct sequence
+ // of states (see StoryState FIDL file for valid state transitions). Since we
+ // started it, ran it, and stopped it, the sequence is STOPPED -> RUNNING ->
+ // STOPPING -> STOPPED.
+ bool called_detach_view = false;
+ mock_session_shell_.GetSessionShellImpl()->set_on_detach_view(
+ [&called_detach_view](ViewIdentifier) { called_detach_view = true; });
+ fuchsia::modular::StoryControllerPtr story_controller;
+ story_provider->GetController(kStoryId, story_controller.NewRequest());
+ bool stop_called = false;
+ story_controller->Stop([&stop_called] { stop_called = true; });
+ ASSERT_TRUE(
+ RunLoopWithTimeoutOrUntil([&] { return stop_called; }, zx::sec(10)));
+ // Run the loop until there are the expected number of state changes;
+ // having called Stop() is not enough to guarantee seeing all updates.
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
+ [&] { return sequence_of_story_states.size() == 4; }, zx::sec(10)));
+ EXPECT_TRUE(called_detach_view);
+ EXPECT_THAT(sequence_of_story_states,
+ testing::ElementsAre(StoryState::STOPPED, StoryState::RUNNING,
+ StoryState::STOPPING, StoryState::STOPPED));
+}
+
+TEST_F(SessionShellTest, StoryStopDoesntWaitOnDetachView) {
+ RunHarnessAndInterceptSessionShell();
+
+ // Create a new story using PuppetMaster and start a new story shell.
+ // Confirm that AttachView() is called.
+ fuchsia::modular::PuppetMasterPtr puppet_master;
+ fuchsia::modular::StoryPuppetMasterPtr story_master;
+
+ fuchsia::modular::testing::TestHarnessService svc;
+ svc.set_puppet_master(puppet_master.NewRequest());
+ test_harness()->GetService(std::move(svc));
+
+ fuchsia::modular::StoryProvider* story_provider =
+ mock_session_shell_.GetStoryProvider();
+ ASSERT_TRUE(story_provider != nullptr);
+ const char kStoryId[] = "my_story";
+
+ // Have the mock session_shell record the sequence of story states it sees,
+ // and confirm that it only sees the correct story id.
+ std::vector<StoryState> sequence_of_story_states;
+ mock_session_shell_.SetOnChange(
+ [&sequence_of_story_states, kStoryId](StoryInfo story_info,
+ StoryState story_state,
+ StoryVisibilityState _) {
+ EXPECT_EQ(story_info.id, kStoryId);
+ sequence_of_story_states.push_back(story_state);
+ });
+ puppet_master->ControlStory(kStoryId, story_master.NewRequest());
+
+ AddMod add_mod;
+ add_mod.mod_name_transitional = "mod1";
+ add_mod.intent.handler = kFakeModuleUrl;
+
+ StoryCommand command;
+ command.set_add_mod(std::move(add_mod));
+
+ std::vector<StoryCommand> commands;
+ commands.push_back(std::move(command));
+
+ story_master->Enqueue(std::move(commands));
+ story_master->Execute([](fuchsia::modular::ExecuteResult result) {});
+
+ bool called_attach_view = false;
+ mock_session_shell_.GetSessionShellImpl()->set_on_attach_view(
+ [&called_attach_view](ViewIdentifier) { called_attach_view = true; });
+
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return called_attach_view; },
+ zx::sec(10)));
+
+ // Stop the story. Confirm that:
+ // a. The story stopped, even though it didn't see the DetachView() response
+ // (it was artificially delayed for 1hr).
+ // b. The story went through the correct sequence of states (see StoryState
+ // FIDL file for valid state transitions). Since we started it, ran it, and
+ // stopped it, the sequence is STOPPED -> RUNNING -> STOPPING -> STOPPED.
+ mock_session_shell_.GetSessionShellImpl()->set_detach_delay(zx::sec(60 * 60));
+ fuchsia::modular::StoryControllerPtr story_controller;
+ story_provider->GetController(kStoryId, story_controller.NewRequest());
+ bool stop_called = false;
+ story_controller->Stop([&stop_called] { stop_called = true; });
+
+ ASSERT_TRUE(
+ RunLoopWithTimeoutOrUntil([&] { return stop_called; }, zx::sec(10)));
+ // Run the loop until there are the expected number of state changes;
+ // having called Stop() is not enough to guarantee seeing all updates.
+ ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
+ [&] { return sequence_of_story_states.size() == 4; }, zx::sec(10)));
+ EXPECT_THAT(sequence_of_story_states,
+ testing::ElementsAre(StoryState::STOPPED, StoryState::RUNNING,
+ StoryState::STOPPING, StoryState::STOPPED));
+}
+
+// TODO(MF-399): Add a test that ensures DetachView() is not called on logout.
+// This will likely require mocking the base shell as well.
+} // namespace