[scenic] Add view state tests

Add end to end view state tests.  View state tests run a full scenic
application via the |EmbedderView| utility, and verify that they
successfully produce a |ViewStateChangedEvent|.

Follow up work involves testing additional apps (for now we just test
the bouncing_ball app), and verifying more stuff (e.g., pixel test that
the screen is not black when we know the screen should be "something").
Let's keep the scope small for now though.

A big thanks to Mike and Mikael for pair collaborating on this, and
also Ross for writing the scenic pixel tests, which this test is heavily
based on.

Test: CQ
fx run-test scenic_tests -t gfx_viewstate_apptests

Change-Id: Ia12ac0b6e90b9db2c20ee411c35738540ab8dd61
diff --git a/bin/ui/BUILD.gn b/bin/ui/BUILD.gn
index 82bc475..c7344e8 100644
--- a/bin/ui/BUILD.gn
+++ b/bin/ui/BUILD.gn
@@ -42,6 +42,16 @@
       ]
     },
     {
+      name = "gfx_viewstate_apptests"
+      environments = [
+        {
+          dimensions = {
+            device_type = "Intel NUC Kit NUC7i5DNHE"
+          }
+        },
+      ]
+    },
+    {
       name = "gfx_unittests"
     },
     {
diff --git a/bin/ui/meta/gfx_view_state_tests.cmx b/bin/ui/meta/gfx_view_state_tests.cmx
new file mode 100644
index 0000000..5f3fa26
--- /dev/null
+++ b/bin/ui/meta/gfx_view_state_tests.cmx
@@ -0,0 +1,14 @@
+{
+    "program": {
+        "binary": "test/gfx_view_state_tests"
+    },
+    "sandbox": {
+        "features": [
+            "system-temp"
+        ],
+        "services": [
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Loader"
+        ]
+    }
+}
diff --git a/examples/ui/hello_base_view/view.cc b/examples/ui/hello_base_view/view.cc
index c8c2a3b..6061ac3 100644
--- a/examples/ui/hello_base_view/view.cc
+++ b/examples/ui/hello_base_view/view.cc
@@ -32,7 +32,8 @@
 void ShadertoyEmbedderView::LaunchShadertoyClient() {
   FXL_DCHECK(!view_holder_);
 
-  embedded_view_info_ = LaunchComponentAndCreateView(
+  embedded_view_info_ = scenic::LaunchComponentAndCreateView(
+      startup_context()->launcher(),
       "fuchsia-pkg://fuchsia.com/shadertoy_client#meta/shadertoy_client.cmx");
 
   view_holder_ = std::make_unique<scenic::ViewHolder>(
diff --git a/examples/ui/hello_base_view/view.h b/examples/ui/hello_base_view/view.h
index b103f69..a702a79 100644
--- a/examples/ui/hello_base_view/view.h
+++ b/examples/ui/hello_base_view/view.h
@@ -43,7 +43,7 @@
   scenic::EntityNode node_;
   scenic::ShapeNode background_;
 
-  scenic::BaseView::EmbeddedViewInfo embedded_view_info_;
+  scenic::EmbeddedViewInfo embedded_view_info_;
   std::unique_ptr<scenic::ViewHolder> view_holder_;
 
   bool focused_;
diff --git a/lib/ui/gfx/tests/BUILD.gn b/lib/ui/gfx/tests/BUILD.gn
index 5f1f376..36cd31c 100644
--- a/lib/ui/gfx/tests/BUILD.gn
+++ b/lib/ui/gfx/tests/BUILD.gn
@@ -10,6 +10,7 @@
     ":apptests",
     ":pixeltests",
     ":unittests",
+    ":viewstate_apptests",
   ]
 }
 
@@ -133,6 +134,34 @@
   ]
 }
 
+executable("viewstate_apptests") {
+  output_name = "gfx_viewstate_apptests"
+
+  testonly = true
+  sources = [
+    "gfx_viewstate_apptest.cc",
+  ]
+  include_dirs = [
+    "//garnet/public/lib/escher",
+    "//third_party/glm",
+  ]
+  deps = [
+    "//garnet/public/fidl/fuchsia.sys",
+    "//garnet/public/fidl/fuchsia.ui.app",
+    "//garnet/public/fidl/fuchsia.ui.policy",
+    "//garnet/public/fidl/fuchsia.ui.scenic",
+    "//garnet/public/lib/component/cpp/testing",
+    "//garnet/public/lib/fsl",
+    "//garnet/public/lib/fxl",
+    "//garnet/public/lib/gtest",
+    "//garnet/public/lib/ui/base_view/cpp",
+    "//garnet/public/lib/ui/gfx/cpp",
+    "//garnet/public/lib/ui/scenic/cpp",
+    "//third_party/googletest:gtest_main",
+    "//zircon/public/lib/async-cpp",
+  ]
+}
+
 executable("mock_pose_buffer_provider_cc") {
   output_name = "mock_pose_buffer_provider"
 
diff --git a/lib/ui/gfx/tests/gfx_viewstate_apptest.cc b/lib/ui/gfx/tests/gfx_viewstate_apptest.cc
new file mode 100644
index 0000000..aa0ed3e
--- /dev/null
+++ b/lib/ui/gfx/tests/gfx_viewstate_apptest.cc
@@ -0,0 +1,175 @@
+// Copyright 2019 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/ui/app/cpp/fidl.h>
+#include <fuchsia/ui/gfx/cpp/fidl.h>
+#include <fuchsia/ui/input/cpp/fidl.h>
+#include <fuchsia/ui/policy/cpp/fidl.h>
+#include <fuchsia/ui/scenic/cpp/fidl.h>
+#include <gtest/gtest.h>
+#include <lib/async/cpp/task.h>
+#include <lib/component/cpp/testing/test_with_environment.h>
+#include <lib/fsl/vmo/vector.h>
+#include <lib/fxl/logging.h>
+#include <lib/fxl/strings/string_printf.h>
+#include <lib/ui/base_view/cpp/base_view.h>
+#include <lib/ui/base_view/cpp/embedded_view_utils.h>
+#include <lib/ui/gfx/cpp/math.h>
+#include <lib/ui/scenic/cpp/resources.h>
+#include <lib/ui/scenic/cpp/session.h>
+
+#include "lib/component/cpp/startup_context.h"
+#include "lib/svc/cpp/services.h"
+#include "lib/ui/scenic/cpp/resources.h"
+#include "lib/ui/scenic/cpp/session.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace {
+
+struct ViewContext {
+  scenic::SessionPtrAndListenerRequest session_and_listener_request;
+  zx::eventpair view_token;
+};
+
+// clang-format off
+const std::map<std::string, std::string> kServices = {
+    {"fuchsia.tracelink.Registry", "fuchsia-pkg://fuchsia.com/trace_manager#meta/trace_manager.cmx"},
+    {"fuchsia.ui.policy.Presenter2", "fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx"},
+    {"fuchsia.ui.scenic.Scenic", "fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx"},
+    {"fuchsia.vulkan.loader.Loader", "fuchsia-pkg://fuchsia.com/vulkan_loader#meta/vulkan_loader.cmx"},
+};
+// clang-format on
+
+class EmbedderView : public fuchsia::ui::scenic::SessionListener {
+ public:
+  EmbedderView(ViewContext context, scenic::EmbeddedViewInfo info,
+               const std::string& debug_name = "EmbedderView")
+      : binding_(this, std::move(context.session_and_listener_request.second)),
+        session_(std::move(context.session_and_listener_request.first)),
+        view_(&session_, std::move(context.view_token), debug_name),
+        embedded_view_info_(std::move(info)),
+        view_holder_(&session_,
+                     std::move(embedded_view_info_.view_holder_token),
+                     debug_name + " ViewHolder"),
+        top_node_(&session_) {
+    binding_.set_error_handler([](zx_status_t status) { FAIL() << status; });
+    view_.AddChild(top_node_);
+    top_node_.Attach(view_holder_);
+    // Call |Session::Present| in order to flush events having to do with
+    // creation of |view_| and |top_node_|.
+    session_.Present(0, [](auto) {});
+  }
+
+  void SetViewStateChangedCallback(
+      std::function<void(fuchsia::ui::gfx::ViewState)> callback) {
+    view_state_changed_callback_ = std::move(callback);
+  }
+
+ private:
+  void OnScenicEvent(std::vector<fuchsia::ui::scenic::Event> events) override {
+    for (const auto& event : events) {
+      if (event.Which() == fuchsia::ui::scenic::Event::Tag::kGfx &&
+          event.gfx().Which() ==
+              fuchsia::ui::gfx::Event::Tag::kViewPropertiesChanged) {
+        const auto& evt = event.gfx().view_properties_changed();
+        view_holder_.SetViewProperties(std::move(evt.properties));
+        session_.Present(0, [](auto) {});
+      } else if (event.Which() == fuchsia::ui::scenic::Event::Tag::kGfx &&
+                 event.gfx().Which() ==
+                     fuchsia::ui::gfx::Event::Tag::kViewStateChanged) {
+        const auto& evt = event.gfx().view_state_changed();
+        if (evt.view_holder_id == view_holder_.id()) {
+          // Clients of |EmbedderView| *must* set a view state changed
+          // callback.  Failure to do so is a usage error.
+          FXL_DCHECK(view_state_changed_callback_);
+          view_state_changed_callback_(evt.state);
+        }
+      }
+    }
+  }
+
+  void OnScenicError(std::string error) override { FAIL() << error; }
+
+  fidl::Binding<fuchsia::ui::scenic::SessionListener> binding_;
+  scenic::Session session_;
+  scenic::View view_;
+  scenic::EmbeddedViewInfo embedded_view_info_;
+  scenic::ViewHolder view_holder_;
+  scenic::EntityNode top_node_;
+  std::function<void(fuchsia::ui::gfx::ViewState)> view_state_changed_callback_;
+};
+
+// Test fixture that sets up an environment suitable for Scenic pixel tests
+// and provides related utilities. The environment includes Scenic and
+// RootPresenter, and their dependencies.
+class ViewEmbedderTest : public component::testing::TestWithEnvironment {
+ protected:
+  ViewEmbedderTest() {
+    std::unique_ptr<component::testing::EnvironmentServices> services =
+        CreateServices();
+
+    for (const auto& [service_name, url] : kServices) {
+      fuchsia::sys::LaunchInfo launch_info;
+      launch_info.url = url;
+      services->AddServiceWithLaunchInfo(std::move(launch_info), service_name);
+    }
+
+    constexpr char kEnvironment[] = "ViewEmbedderTest";
+    environment_ =
+        CreateNewEnclosingEnvironment(kEnvironment, std::move(services));
+
+    environment_->ConnectToService(scenic_.NewRequest());
+    scenic_.set_error_handler([this](zx_status_t status) {
+      FAIL() << "Lost connection to Scenic: " << status;
+    });
+  }
+
+  // Create a |ViewContext| that allows us to present a view via
+  // |RootPresenter|. See also examples/ui/hello_base_view
+  ViewContext CreatePresentationContext() {
+    zx::eventpair view_holder_token, view_token;
+    FXL_CHECK(zx::eventpair::create(0u, &view_holder_token, &view_token) ==
+              ZX_OK);
+
+    ViewContext view_context = {
+        .session_and_listener_request =
+            scenic::CreateScenicSessionPtrAndListenerRequest(scenic_.get()),
+        .view_token = std::move(view_token),
+    };
+
+    fuchsia::ui::policy::Presenter2Ptr presenter;
+    environment_->ConnectToService(presenter.NewRequest());
+    presenter->PresentView(std::move(view_holder_token), nullptr);
+
+    return view_context;
+  }
+
+  fuchsia::ui::scenic::ScenicPtr scenic_;
+  std::unique_ptr<component::testing::EnclosingEnvironment> environment_;
+};
+
+TEST_F(ViewEmbedderTest, BouncingBall) {
+  auto info = scenic::LaunchComponentAndCreateView(
+      environment_->launcher_ptr(),
+      "fuchsia-pkg://fuchsia.com/bouncing_ball#meta/bouncing_ball.cmx", {});
+
+  EmbedderView embedder_view(CreatePresentationContext(), std::move(info));
+
+  // Run the loop until we observe the view state changing, or hit a 10 second
+  // timeout.
+  bool view_state_changed_observed = false;
+  embedder_view.SetViewStateChangedCallback(
+      [this, &view_state_changed_observed](auto view_state) {
+        view_state_changed_observed = true;
+        async::PostTask(dispatcher(), QuitLoopClosure());
+      });
+  RunLoopWithTimeout(zx::sec(10));
+
+  EXPECT_TRUE(view_state_changed_observed);
+}
+
+}  // namespace
diff --git a/public/lib/ui/base_view/cpp/BUILD.gn b/public/lib/ui/base_view/cpp/BUILD.gn
index bcc0593..c875514 100644
--- a/public/lib/ui/base_view/cpp/BUILD.gn
+++ b/public/lib/ui/base_view/cpp/BUILD.gn
@@ -6,6 +6,8 @@
   sources = [
     "base_view.cc",
     "base_view.h",
+    "embedded_view_utils.cc",
+    "embedded_view_utils.h",
     "v1_base_view.cc",
     "v1_base_view.h",
     "view_provider_component.cc",
diff --git a/public/lib/ui/base_view/cpp/base_view.cc b/public/lib/ui/base_view/cpp/base_view.cc
index 8abedac..2d69ac6 100644
--- a/public/lib/ui/base_view/cpp/base_view.cc
+++ b/public/lib/ui/base_view/cpp/base_view.cc
@@ -40,37 +40,6 @@
   listener_binding_.set_error_handler(std::move(callback));
 }
 
-BaseView::EmbeddedViewInfo BaseView::LaunchComponentAndCreateView(
-    std::string component_url, std::vector<std::string> component_args) {
-  auto& launcher = startup_context_->launcher();
-  FXL_DCHECK(launcher) << "no Launcher available.";
-
-  zx::eventpair view_holder_token, view_token;
-  auto status = zx::eventpair::create(0u, &view_holder_token, &view_token);
-  FXL_DCHECK(status == ZX_OK) << "failed to create tokens.";
-
-  EmbeddedViewInfo info;
-  info.view_holder_token = std::move(view_holder_token);
-
-  launcher->CreateComponent(
-      {.url = component_url,
-       .arguments = fidl::VectorPtr(std::vector<std::string>(
-           component_args.begin(), component_args.end())),
-       .directory_request = info.app_services.NewRequest()},
-      info.controller.NewRequest());
-
-  info.app_services.ConnectToService(info.view_provider.NewRequest());
-
-  fidl::InterfaceHandle<fuchsia::sys::ServiceProvider> services_to_child_view;
-  info.services_to_child_view = services_to_child_view.NewRequest();
-
-  info.view_provider->CreateView(std::move(view_token),
-                                 info.services_from_child_view.NewRequest(),
-                                 std::move(services_to_child_view));
-
-  return info;
-}
-
 void BaseView::InvalidateScene() {
   if (invalidate_pending_)
     return;
diff --git a/public/lib/ui/base_view/cpp/base_view.h b/public/lib/ui/base_view/cpp/base_view.h
index 4fdea10..55537d4 100644
--- a/public/lib/ui/base_view/cpp/base_view.h
+++ b/public/lib/ui/base_view/cpp/base_view.h
@@ -12,6 +12,7 @@
 
 #include "lib/component/cpp/startup_context.h"
 #include "lib/svc/cpp/services.h"
+#include "lib/ui/base_view/cpp/embedded_view_utils.h"
 #include "lib/ui/scenic/cpp/resources.h"
 #include "lib/ui/scenic/cpp/session.h"
 
@@ -91,48 +92,6 @@
   // associated with the view (including the object itself).
   void SetReleaseHandler(fit::function<void(zx_status_t)> callback);
 
-  // Serves as the return value for LaunchAppAndCreateView(), below.
-  struct EmbeddedViewInfo {
-    // Controls the launched app.  The app will be destroyed if this connection
-    // is closed.
-    fuchsia::sys::ComponentControllerPtr controller;
-
-    // Services provided by the launched app.  Must not be destroyed
-    // immediately, otherwise the |view_provider| connection may not be
-    // established.
-    component::Services app_services;
-
-    // ViewProvider service obtained from the app via |app_services|.  Must not
-    // be destroyed immediately, otherwise the call to CreateView() might not
-    // be processed.
-    fuchsia::ui::app::ViewProviderPtr view_provider;
-
-    // A token that can be used to create a ViewHolder; the corresponding token
-    // was provided to |view_provider| via ViewProvider.CreateView().  The
-    // launched app is expected to create a View, which will be connected to
-    // the ViewHolder created with this token.
-    zx::eventpair view_holder_token;
-
-    // Handle to services provided by ViewProvider.CreateView().
-    fidl::InterfaceHandle<fuchsia::sys::ServiceProvider>
-        services_from_child_view;
-
-    // Interface request for services provided to ViewProvider.CreateView();
-    // the caller of LaunchAppAndCreateView() may choose to attach this request
-    // to a ServiceProvider implementation.
-    fidl::InterfaceRequest<fuchsia::sys::ServiceProvider>
-        services_to_child_view;
-  };
-
-  // Launch a component and connect to its ViewProvider service, passing it the
-  // necessary information to attach itself as a child view.  Populates the
-  // returned EmbeddedViewInfo, which the caller can use to embed the child.
-  // For example, an interface to a ViewProvider is obtained, a pair of
-  // zx::eventpairs is created, CreateView is called, etc.  This encapsulates
-  // the boilerplate the the client would otherwise write themselves.
-  EmbeddedViewInfo LaunchComponentAndCreateView(
-      std::string component_url, std::vector<std::string> component_args = {});
-
   // Invalidates the scene, causing |OnSceneInvalidated()| to be invoked
   // during the next frame.
   void InvalidateScene();
diff --git a/public/lib/ui/base_view/cpp/embedded_view_utils.cc b/public/lib/ui/base_view/cpp/embedded_view_utils.cc
new file mode 100644
index 0000000..f06c042
--- /dev/null
+++ b/public/lib/ui/base_view/cpp/embedded_view_utils.cc
@@ -0,0 +1,42 @@
+// Copyright 2019 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 "lib/ui/base_view/cpp/embedded_view_utils.h"
+
+#include "lib/fxl/logging.h"
+
+namespace scenic {
+
+EmbeddedViewInfo LaunchComponentAndCreateView(
+    const fuchsia::sys::LauncherPtr& launcher, const std::string& component_url,
+    const std::vector<std::string>& component_args) {
+  FXL_DCHECK(launcher);
+
+  zx::eventpair view_holder_token, view_token;
+  auto status = zx::eventpair::create(0u, &view_holder_token, &view_token);
+  FXL_DCHECK(status == ZX_OK) << "failed to create tokens.";
+
+  EmbeddedViewInfo info;
+  info.view_holder_token = std::move(view_holder_token);
+
+  launcher->CreateComponent(
+      {.url = component_url,
+       .arguments = fidl::VectorPtr(std::vector<std::string>(
+           component_args.begin(), component_args.end())),
+       .directory_request = info.app_services.NewRequest()},
+      info.controller.NewRequest());
+
+  info.app_services.ConnectToService(info.view_provider.NewRequest());
+
+  fidl::InterfaceHandle<fuchsia::sys::ServiceProvider> services_to_child_view;
+  info.services_to_child_view = services_to_child_view.NewRequest();
+
+  info.view_provider->CreateView(std::move(view_token),
+                                 info.services_from_child_view.NewRequest(),
+                                 std::move(services_to_child_view));
+
+  return info;
+}
+
+}  // namespace scenic
diff --git a/public/lib/ui/base_view/cpp/embedded_view_utils.h b/public/lib/ui/base_view/cpp/embedded_view_utils.h
new file mode 100644
index 0000000..95802c2
--- /dev/null
+++ b/public/lib/ui/base_view/cpp/embedded_view_utils.h
@@ -0,0 +1,63 @@
+// Copyright 2019 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.
+
+#ifndef LIB_UI_BASE_VIEW_CPP_EMBEDDED_VIEW_UTILS_H_
+#define LIB_UI_BASE_VIEW_CPP_EMBEDDED_VIEW_UTILS_H_
+
+#include <fuchsia/ui/app/cpp/fidl.h>
+#include <fuchsia/ui/gfx/cpp/fidl.h>
+#include <fuchsia/ui/input/cpp/fidl.h>
+#include <fuchsia/ui/scenic/cpp/fidl.h>
+
+#include "lib/component/cpp/startup_context.h"
+#include "lib/svc/cpp/services.h"
+#include "lib/ui/scenic/cpp/resources.h"
+#include "lib/ui/scenic/cpp/session.h"
+
+namespace scenic {
+
+// Serves as the return value for LaunchAppAndCreateView(), below.
+struct EmbeddedViewInfo {
+  // Controls the launched app.  The app will be destroyed if this connection
+  // is closed.
+  fuchsia::sys::ComponentControllerPtr controller;
+
+  // Services provided by the launched app.  Must not be destroyed
+  // immediately, otherwise the |view_provider| connection may not be
+  // established.
+  component::Services app_services;
+
+  // ViewProvider service obtained from the app via |app_services|.  Must not
+  // be destroyed immediately, otherwise the call to CreateView() might not be
+  // processed.
+  fuchsia::ui::app::ViewProviderPtr view_provider;
+
+  // A token that can be used to create a ViewHolder; the corresponding token
+  // was provided to |view_provider| via ViewProvider.CreateView().  The
+  // launched app is expected to create a View, which will be connected to the
+  // ViewHolder created with this token.
+  zx::eventpair view_holder_token;
+
+  // Handle to services provided by ViewProvider.CreateView().
+  fidl::InterfaceHandle<fuchsia::sys::ServiceProvider> services_from_child_view;
+
+  // Interface request for services provided to ViewProvider.CreateView(); the
+  // caller of LaunchAppAndCreateView() may choose to attach this request to a
+  // ServiceProvider implementation.
+  fidl::InterfaceRequest<fuchsia::sys::ServiceProvider> services_to_child_view;
+};
+
+// Launch a component and connect to its ViewProvider service, passing it the
+// necessary information to attach itself as a child view.  Populates the
+// returned EmbeddedViewInfo, which the caller can use to embed the child.
+// For example, an interface to a ViewProvider is obtained, a pair of
+// zx::eventpairs is created, CreateView is called, etc.  This encapsulates
+// the boilerplate the the client would otherwise write themselves.
+EmbeddedViewInfo LaunchComponentAndCreateView(
+    const fuchsia::sys::LauncherPtr& launcher, const std::string& component_url,
+    const std::vector<std::string>& component_args = {});
+
+}  // namespace scenic
+
+#endif  // LIB_UI_BASE_VIEW_CPP_EMBEDDED_VIEW_UTILS_H_