[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